class FleetVehicle(models.Model): _inherit = 'mail.thread' _name = 'fleet.vehicle' _description = 'Information on a vehicle' _order = 'license_plate asc' def _get_default_state(self): state = self.env.ref('fleet.vehicle_state_active', raise_if_not_found=False) return state and state.id or False name = fields.Char(compute="_compute_vehicle_name", store=True) active = fields.Boolean('Active', default=True, track_visibility="onchange") company_id = fields.Many2one('res.company', 'Company') license_plate = fields.Char( required=True, help='License plate number of the vehicle (i = plate number for a car)' ) vin_sn = fields.Char( 'Chassis Number', help='Unique number written on the vehicle motor (VIN/SN number)', copy=False) driver_id = fields.Many2one('res.partner', 'Driver', help='Driver of the vehicle', copy=False) model_id = fields.Many2one('fleet.vehicle.model', 'Model', required=True, help='Model of the vehicle') log_fuel = fields.One2many('fleet.vehicle.log.fuel', 'vehicle_id', 'Fuel Logs') log_services = fields.One2many('fleet.vehicle.log.services', 'vehicle_id', 'Services Logs') log_contracts = fields.One2many('fleet.vehicle.log.contract', 'vehicle_id', 'Contracts') cost_count = fields.Integer(compute="_compute_count_all", string="Costs") contract_count = fields.Integer(compute="_compute_count_all", string='Contracts') service_count = fields.Integer(compute="_compute_count_all", string='Services') fuel_logs_count = fields.Integer(compute="_compute_count_all", string='Fuel Logs') odometer_count = fields.Integer(compute="_compute_count_all", string='Odometer') acquisition_date = fields.Date( 'Immatriculation Date', required=False, help='Date when the vehicle has been immatriculated') color = fields.Char(help='Color of the vehicle') state_id = fields.Many2one('fleet.vehicle.state', 'State', default=_get_default_state, help='Current state of the vehicle', ondelete="set null") location = fields.Char(help='Location of the vehicle (garage, ...)') seats = fields.Integer('Seats Number', help='Number of seats of the vehicle') model_year = fields.Char('Model Year', help='Year of the model') doors = fields.Integer('Doors Number', help='Number of doors of the vehicle', default=5) tag_ids = fields.Many2many('fleet.vehicle.tag', 'fleet_vehicle_vehicle_tag_rel', 'vehicle_tag_id', 'tag_id', 'Tags', copy=False) odometer = fields.Float( compute='_get_odometer', inverse='_set_odometer', string='Last Odometer', help='Odometer measure of the vehicle at the moment of this log') odometer_unit = fields.Selection([('kilometers', 'Kilometers'), ('miles', 'Miles')], 'Odometer Unit', default='kilometers', help='Unit of the odometer ', required=True) transmission = fields.Selection([('manual', 'Manual'), ('automatic', 'Automatic')], 'Transmission', help='Transmission Used by the vehicle') fuel_type = fields.Selection([('gasoline', 'Gasoline'), ('diesel', 'Diesel'), ('electric', 'Electric'), ('hybrid', 'Hybrid')], 'Fuel Type', help='Fuel Used by the vehicle') horsepower = fields.Integer() horsepower_tax = fields.Float('Horsepower Taxation') power = fields.Integer('Power', help='Power in kW of the vehicle') co2 = fields.Float('CO2 Emissions', help='CO2 emissions of the vehicle') image = fields.Binary(related='model_id.image', string="Logo") image_medium = fields.Binary(related='model_id.image_medium', string="Logo (medium)") image_small = fields.Binary(related='model_id.image_small', string="Logo (small)") contract_renewal_due_soon = fields.Boolean( compute='_compute_contract_reminder', search='_search_contract_renewal_due_soon', string='Has Contracts to renew', multi='contract_info') contract_renewal_overdue = fields.Boolean( compute='_compute_contract_reminder', search='_search_get_overdue_contract_reminder', string='Has Contracts Overdue', multi='contract_info') contract_renewal_name = fields.Text( compute='_compute_contract_reminder', string='Name of contract to renew soon', multi='contract_info') contract_renewal_total = fields.Text( compute='_compute_contract_reminder', string='Total of contracts due or overdue minus one', multi='contract_info') car_value = fields.Float(string="Catalog Value (VAT Incl.)", help='Value of the bought vehicle') residual_value = fields.Float() _sql_constraints = [('driver_id_unique', 'UNIQUE(driver_id)', 'Only one car can be assigned to the same employee!')] @api.depends('model_id', 'license_plate') def _compute_vehicle_name(self): for record in self: record.name = record.model_id.brand_id.name + '/' + record.model_id.name + '/' + record.license_plate def _get_odometer(self): FleetVehicalOdometer = self.env['fleet.vehicle.odometer'] for record in self: vehicle_odometer = FleetVehicalOdometer.search( [('vehicle_id', '=', record.id)], limit=1, order='value desc') if vehicle_odometer: record.odometer = vehicle_odometer.value else: record.odometer = 0 def _set_odometer(self): for record in self: if record.odometer: date = fields.Date.context_today(record) data = { 'value': record.odometer, 'date': date, 'vehicle_id': record.id } self.env['fleet.vehicle.odometer'].create(data) def _compute_count_all(self): Odometer = self.env['fleet.vehicle.odometer'] LogFuel = self.env['fleet.vehicle.log.fuel'] LogService = self.env['fleet.vehicle.log.services'] LogContract = self.env['fleet.vehicle.log.contract'] Cost = self.env['fleet.vehicle.cost'] for record in self: record.odometer_count = Odometer.search_count([('vehicle_id', '=', record.id)]) record.fuel_logs_count = LogFuel.search_count([('vehicle_id', '=', record.id)]) record.service_count = LogService.search_count([('vehicle_id', '=', record.id)]) record.contract_count = LogContract.search_count([ ('vehicle_id', '=', record.id), ('state', '!=', 'closed') ]) record.cost_count = Cost.search_count([('vehicle_id', '=', record.id), ('parent_id', '=', False)]) @api.depends('log_contracts') def _compute_contract_reminder(self): for record in self: overdue = False due_soon = False total = 0 name = '' for element in record.log_contracts: if element.state in ('open', 'expired') and element.expiration_date: current_date_str = fields.Date.context_today(record) due_time_str = element.expiration_date current_date = fields.Date.from_string(current_date_str) due_time = fields.Date.from_string(due_time_str) diff_time = (due_time - current_date).days if diff_time < 0: overdue = True total += 1 if diff_time < 15 and diff_time >= 0: due_soon = True total += 1 if overdue or due_soon: log_contract = self.env[ 'fleet.vehicle.log.contract'].search( [('vehicle_id', '=', record.id), ('state', 'in', ('open', 'expired'))], limit=1, order='expiration_date asc') if log_contract: # we display only the name of the oldest overdue/due soon contract name = log_contract.cost_subtype_id.name record.contract_renewal_overdue = overdue record.contract_renewal_due_soon = due_soon record.contract_renewal_total = total - 1 # we remove 1 from the real total for display purposes record.contract_renewal_name = name def _search_contract_renewal_due_soon(self, operator, value): res = [] assert operator in ('=', '!=', '<>') and value in ( True, False), 'Operation not supported' if (operator == '=' and value is True) or (operator in ('<>', '!=') and value is False): search_operator = 'in' else: search_operator = 'not in' today = fields.Date.context_today(self) datetime_today = fields.Datetime.from_string(today) limit_date = fields.Datetime.to_string(datetime_today + relativedelta(days=+15)) self.env.cr.execute( """SELECT cost.vehicle_id, count(contract.id) AS contract_number FROM fleet_vehicle_cost cost LEFT JOIN fleet_vehicle_log_contract contract ON contract.cost_id = cost.id WHERE contract.expiration_date IS NOT NULL AND contract.expiration_date > %s AND contract.expiration_date < %s AND contract.state IN ('open', 'expired') GROUP BY cost.vehicle_id""", (today, limit_date)) res_ids = [x[0] for x in self.env.cr.fetchall()] res.append(('id', search_operator, res_ids)) return res def _search_get_overdue_contract_reminder(self, operator, value): res = [] assert operator in ('=', '!=', '<>') and value in ( True, False), 'Operation not supported' if (operator == '=' and value is True) or (operator in ('<>', '!=') and value is False): search_operator = 'in' else: search_operator = 'not in' today = fields.Date.context_today(self) self.env.cr.execute( '''SELECT cost.vehicle_id, count(contract.id) AS contract_number FROM fleet_vehicle_cost cost LEFT JOIN fleet_vehicle_log_contract contract ON contract.cost_id = cost.id WHERE contract.expiration_date IS NOT NULL AND contract.expiration_date < %s AND contract.state IN ('open', 'expired') GROUP BY cost.vehicle_id ''', (today, )) res_ids = [x[0] for x in self.env.cr.fetchall()] res.append(('id', search_operator, res_ids)) return res @api.onchange('model_id') def _onchange_model(self): if self.model_id: self.image_medium = self.model_id.image else: self.image_medium = False @api.model def create(self, data): vehicle = super(FleetVehicle, self.with_context(mail_create_nolog=True)).create(data) vehicle.message_post(body=_('%s %s has been added to the fleet!') % (vehicle.model_id.name, vehicle.license_plate)) return vehicle @api.multi def write(self, vals): """ This function write an entry in the openchatter whenever we change important information on the vehicle like the model, the drive, the state of the vehicle or its license plate """ for vehicle in self: changes = [] if 'model_id' in vals and vehicle.model_id.id != vals['model_id']: value = self.env['fleet.vehicle.model'].browse( vals['model_id']).name oldmodel = vehicle.model_id.name or _('None') changes.append( _("Model: from '%s' to '%s'") % (oldmodel, value)) if 'driver_id' in vals and vehicle.driver_id.id != vals[ 'driver_id']: value = self.env['res.partner'].browse(vals['driver_id']).name olddriver = (vehicle.driver_id.name) or _('None') changes.append( _("Driver: from '%s' to '%s'") % (olddriver, value)) if 'state_id' in vals and vehicle.state_id.id != vals['state_id']: value = self.env['fleet.vehicle.state'].browse( vals['state_id']).name oldstate = vehicle.state_id.name or _('None') changes.append( _("State: from '%s' to '%s'") % (oldstate, value)) if 'license_plate' in vals and vehicle.license_plate != vals[ 'license_plate']: old_license_plate = vehicle.license_plate or _('None') changes.append( _("License Plate: from '%s' to '%s'") % (old_license_plate, vals['license_plate'])) if len(changes) > 0: self.message_post(body=", ".join(changes)) return super(FleetVehicle, self).write(vals) @api.multi def return_action_to_open(self): """ This opens the xml view specified in xml_id for the current vehicle """ self.ensure_one() xml_id = self.env.context.get('xml_id') if xml_id: res = self.env['ir.actions.act_window'].for_xml_id('fleet', xml_id) res.update(context=dict(self.env.context, default_vehicle_id=self.id, group_by=False), domain=[('vehicle_id', '=', self.id)]) return res return False @api.multi def act_show_log_cost(self): """ This opens log view to view and add new log for this vehicle, groupby default to only show effective costs @return: the costs log view """ self.ensure_one() copy_context = dict(self.env.context) copy_context.pop('group_by', None) res = self.env['ir.actions.act_window'].for_xml_id( 'fleet', 'fleet_vehicle_costs_action') res.update(context=dict(copy_context, default_vehicle_id=self.id, search_default_parent_false=True), domain=[('vehicle_id', '=', self.id)]) return res
class ImLivechatChannel(models.Model): """ Livechat Channel Define a communication channel, which can be accessed with 'script_external' (script tag to put on external website), 'script_internal' (code to be integrated with actpy website) or via 'web_page' link. It provides rating tools, and access rules for anonymous people. """ _name = 'im_livechat.channel' _description = 'Livechat Channel' def _default_image(self): image_path = modules.get_module_resource('im_livechat', 'static/src/img', 'default.png') return tools.image_resize_image_big( base64.b64encode(open(image_path, 'rb').read())) def _default_user_ids(self): return [(6, 0, [self._uid])] # attribute fields name = fields.Char('Name', required=True, help="The name of the channel") button_text = fields.Char( 'Text of the Button', default='Have a Question? Chat with us.', help="Default text displayed on the Livechat Support Button") default_message = fields.Char( 'Welcome Message', default='How may I help you?', help= "This is an automated 'welcome' message that your visitor will see when they initiate a new conversation." ) input_placeholder = fields.Char('Chat Input Placeholder') # computed fields web_page = fields.Char( 'Web Page', compute='_compute_web_page_link', store=False, readonly=True, help= "URL to a static page where you client can discuss with the operator of the channel." ) are_you_inside = fields.Boolean(string='Are you inside the matrix?', compute='_are_you_inside', store=False, readonly=True) script_external = fields.Text('Script (external)', compute='_compute_script_external', store=False, readonly=True) nbr_channel = fields.Integer('Number of conversation', compute='_compute_nbr_channel', store=False, readonly=True) rating_percentage_satisfaction = fields.Integer( '% Happy', compute='_compute_percentage_satisfaction', store=False, default=-1, help="Percentage of happy ratings over the past 7 days") # images fields image = fields.Binary( 'Image', default=_default_image, attachment=True, help= "This field holds the image used as photo for the group, limited to 1024x1024px." ) image_medium = fields.Binary('Medium', attachment=True, help="Medium-sized photo of the group. It is automatically "\ "resized as a 128x128px image, with aspect ratio preserved. "\ "Use this field in form views or some kanban views.") image_small = fields.Binary('Thumbnail', attachment=True, help="Small-sized photo of the group. It is automatically "\ "resized as a 64x64px image, with aspect ratio preserved. "\ "Use this field anywhere a small image is required.") # relationnal fields user_ids = fields.Many2many('res.users', 'im_livechat_channel_im_user', 'channel_id', 'user_id', string='Operators', default=_default_user_ids) channel_ids = fields.One2many('mail.channel', 'livechat_channel_id', 'Sessions') rule_ids = fields.One2many('im_livechat.channel.rule', 'channel_id', 'Rules') @api.one def _are_you_inside(self): self.are_you_inside = bool( self.env.uid in [u.id for u in self.user_ids]) @api.multi def _compute_script_external(self): view = self.env['ir.model.data'].get_object('im_livechat', 'external_loader') values = { "url": self.env['ir.config_parameter'].sudo().get_param('web.base.url'), "dbname": self._cr.dbname, } for record in self: values["channel_id"] = record.id record.script_external = view.render(values) @api.multi def _compute_web_page_link(self): base_url = self.env['ir.config_parameter'].sudo().get_param( 'web.base.url') for record in self: record.web_page = "%s/im_livechat/support/%i" % (base_url, record.id) @api.multi @api.depends('channel_ids') def _compute_nbr_channel(self): for record in self: record.nbr_channel = len(record.channel_ids) @api.multi @api.depends('channel_ids.rating_ids') def _compute_percentage_satisfaction(self): for record in self: dt = fields.Datetime.to_string(datetime.utcnow() - timedelta(days=7)) repartition = record.channel_ids.rating_get_grades([('create_date', '>=', dt)]) total = sum(repartition.values()) if total > 0: happy = repartition['great'] record.rating_percentage_satisfaction = ( (happy * 100) / total) if happy > 0 else 0 else: record.rating_percentage_satisfaction = -1 @api.model def create(self, vals): tools.image_resize_images(vals) return super(ImLivechatChannel, self).create(vals) @api.multi def write(self, vals): tools.image_resize_images(vals) return super(ImLivechatChannel, self).write(vals) # -------------------------- # Action Methods # -------------------------- @api.multi def action_join(self): self.ensure_one() return self.write({'user_ids': [(4, self._uid)]}) @api.multi def action_quit(self): self.ensure_one() return self.write({'user_ids': [(3, self._uid)]}) @api.multi def action_view_rating(self): """ Action to display the rating relative to the channel, so all rating of the sessions of the current channel :returns : the ir.action 'action_view_rating' with the correct domain """ self.ensure_one() action = self.env['ir.actions.act_window'].for_xml_id( 'im_livechat', 'rating_rating_action_view_livechat_rating') action['domain'] = [('parent_res_id', '=', self.id), ('parent_res_model', '=', 'im_livechat.channel')] return action # -------------------------- # Channel Methods # -------------------------- @api.multi def get_available_users(self): """ get available user of a given channel :retuns : return the res.users having their im_status online """ self.ensure_one() return self.sudo().user_ids.filtered( lambda user: user.im_status == 'online') @api.model def get_mail_channel(self, livechat_channel_id, anonymous_name): """ Return a mail.channel given a livechat channel. It creates one with a connected operator, or return false otherwise :param livechat_channel_id : the identifier if the im_livechat.channel :param anonymous_name : the name of the anonymous person of the channel :type livechat_channel_id : int :type anonymous_name : str :return : channel header :rtype : dict """ # get the avalable user of the channel users = self.sudo().browse(livechat_channel_id).get_available_users() if len(users) == 0: return False # choose the res.users operator and get its partner id user = random.choice(users) operator_partner_id = user.partner_id.id # partner to add to the mail.channel channel_partner_to_add = [(4, operator_partner_id)] if self.env.user and self.env.user.active: # valid session user (not public) channel_partner_to_add.append((4, self.env.user.partner_id.id)) # create the session, and add the link with the given channel mail_channel = self.env["mail.channel"].with_context( mail_create_nosubscribe=False).sudo().create({ 'channel_partner_ids': channel_partner_to_add, 'livechat_channel_id': livechat_channel_id, 'anonymous_name': anonymous_name, 'channel_type': 'livechat', 'name': ', '.join([anonymous_name, user.name]), 'public': 'private', 'email_send': False, }) return mail_channel.sudo().with_context( im_livechat_operator_partner_id=operator_partner_id).channel_info( )[0] @api.model def get_channel_infos(self, channel_id): channel = self.browse(channel_id) return { 'button_text': channel.button_text, 'input_placeholder': channel.input_placeholder, 'default_message': channel.default_message, "channel_name": channel.name, "channel_id": channel.id, } @api.model def get_livechat_info(self, channel_id, username='******'): info = {} info['available'] = len( self.browse(channel_id).get_available_users()) > 0 info['server_url'] = self.env['ir.config_parameter'].sudo().get_param( 'web.base.url') if info['available']: info['options'] = self.sudo().get_channel_infos(channel_id) info['options']["default_username"] = username return info
class Groups(models.Model): _name = "res.groups" _description = "Access Groups" _rec_name = 'full_name' _order = 'name' name = fields.Char(required=True, translate=True) users = fields.Many2many('res.users', 'res_groups_users_rel', 'gid', 'uid') model_access = fields.One2many('ir.model.access', 'group_id', string='Access Controls', copy=True) rule_groups = fields.Many2many('ir.rule', 'rule_group_rel', 'group_id', 'rule_group_id', string='Rules', domain=[('global', '=', False)]) menu_access = fields.Many2many('ir.ui.menu', 'ir_ui_menu_group_rel', 'gid', 'menu_id', string='Access Menu') view_access = fields.Many2many('ir.ui.view', 'ir_ui_view_group_rel', 'group_id', 'view_id', string='Views') comment = fields.Text(translate=True) category_id = fields.Many2one('ir.module.category', string='Application', index=True) color = fields.Integer(string='Color Index') full_name = fields.Char(compute='_compute_full_name', string='Group Name', search='_search_full_name') share = fields.Boolean(string='Share Group', help="Group created to set access rights for sharing data with some users.") is_portal = fields.Boolean('Portal', help="If checked, this group is usable as a portal.") _sql_constraints = [ ('name_uniq', 'unique (category_id, name)', 'The name of the group must be unique within an application!') ] @api.depends('category_id.name', 'name') def _compute_full_name(self): # Important: value must be stored in environment of group, not group1! for group, group1 in pycompat.izip(self, self.sudo()): if group1.category_id: group.full_name = '%s / %s' % (group1.category_id.name, group1.name) else: group.full_name = group1.name def _search_full_name(self, operator, operand): lst = True if isinstance(operand, bool): domains = [[('name', operator, operand)], [('category_id.name', operator, operand)]] if operator in expression.NEGATIVE_TERM_OPERATORS == (not operand): return expression.AND(domains) else: return expression.OR(domains) if isinstance(operand, pycompat.string_types): lst = False operand = [operand] where = [] for group in operand: values = [v for v in group.split('/') if v] group_name = values.pop().strip() category_name = values and '/'.join(values).strip() or group_name group_domain = [('name', operator, lst and [group_name] or group_name)] category_domain = [('category_id.name', operator, lst and [category_name] or category_name)] if operator in expression.NEGATIVE_TERM_OPERATORS and not values: category_domain = expression.OR([category_domain, [('category_id', '=', False)]]) if (operator in expression.NEGATIVE_TERM_OPERATORS) == (not values): sub_where = expression.AND([group_domain, category_domain]) else: sub_where = expression.OR([group_domain, category_domain]) if operator in expression.NEGATIVE_TERM_OPERATORS: where = expression.AND([where, sub_where]) else: where = expression.OR([where, sub_where]) return where @api.model def search(self, args, offset=0, limit=None, order=None, count=False): # add explicit ordering if search is sorted on full_name if order and order.startswith('full_name'): groups = super(Groups, self).search(args) groups = groups.sorted('full_name', reverse=order.endswith('DESC')) groups = groups[offset:offset+limit] if limit else groups[offset:] return len(groups) if count else groups.ids return super(Groups, self).search(args, offset=offset, limit=limit, order=order, count=count) @api.multi def copy(self, default=None): self.ensure_one() default = dict(default or {}, name=_('%s (copy)') % self.name) return super(Groups, self).copy(default) @api.multi def write(self, vals): if 'name' in vals: if vals['name'].startswith('-'): raise UserError(_('The name of the group can not start with "-"')) # invalidate caches before updating groups, since the recomputation of # field 'share' depends on method has_group() self.env['ir.model.access'].call_cache_clearing_methods() self.env['res.users'].has_group.clear_cache(self.env['res.users']) return super(Groups, self).write(vals)
class MrpBom(models.Model): """ Defines bills of material for a product or a product template """ _name = 'mrp.bom' _description = 'Bill of Material' _inherit = ['mail.thread'] _rec_name = 'product_tmpl_id' _order = "sequence" def _get_default_product_uom_id(self): return self.env['product.uom'].search([], limit=1, order='id').id code = fields.Char('Reference') active = fields.Boolean( 'Active', default=True, help= "If the active field is set to False, it will allow you to hide the bills of material without removing it." ) type = fields.Selection([('normal', 'Manufacture this product'), ('phantom', 'Kit')], 'BoM Type', default='normal', required=True) product_tmpl_id = fields.Many2one( 'product.template', 'Product', domain="[('type', 'in', ['product', 'consu'])]", required=True) product_id = fields.Many2one( 'product.product', 'Product Variant', domain= "['&', ('product_tmpl_id', '=', product_tmpl_id), ('type', 'in', ['product', 'consu'])]", help= "If a product variant is defined the BOM is available only for this product." ) bom_line_ids = fields.One2many('mrp.bom.line', 'bom_id', 'BoM Lines', copy=True) product_qty = fields.Float('Quantity', default=1.0, digits=dp.get_precision('Unit of Measure'), required=True) product_uom_id = fields.Many2one( 'product.uom', 'Product Unit of Measure', default=_get_default_product_uom_id, oldname='product_uom', required=True, help= "Unit of Measure (Unit of Measure) is the unit of measurement for the inventory control" ) sequence = fields.Integer( 'Sequence', help= "Gives the sequence order when displaying a list of bills of material." ) routing_id = fields.Many2one( 'mrp.routing', 'Routing', help= "The operations for producing this BoM. When a routing is specified, the production orders will " " be executed through work orders, otherwise everything is processed in the production order itself. " ) ready_to_produce = fields.Selection( [('all_available', 'All components available'), ('asap', 'The components of 1st operation')], string='Manufacturing Readiness', default='asap', required=True) picking_type_id = fields.Many2one( 'stock.picking.type', 'Operation Type', domain=[('code', '=', 'mrp_operation')], help= u"When a procurement has a ‘produce’ route with a operation type set, it will try to create " "a Manufacturing Order for that product using a BoM of the same operation type. That allows " "to define procurement rules which trigger different manufacturing orders with different BoMs." ) company_id = fields.Many2one('res.company', 'Company', default=lambda self: self.env['res.company']. _company_default_get('mrp.bom'), required=True) @api.constrains('product_id', 'product_tmpl_id', 'bom_line_ids') def _check_product_recursion(self): for bom in self: if bom.bom_line_ids.filtered(lambda x: x.product_id.product_tmpl_id == bom.product_tmpl_id): raise ValidationError( _('BoM line product %s should not be same as BoM product.') % bom.display_name) @api.onchange('product_uom_id') def onchange_product_uom_id(self): res = {} if not self.product_uom_id or not self.product_tmpl_id: return if self.product_uom_id.category_id.id != self.product_tmpl_id.uom_id.category_id.id: self.product_uom_id = self.product_tmpl_id.uom_id.id res['warning'] = { 'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.' ) } return res @api.onchange('product_tmpl_id') def onchange_product_tmpl_id(self): if self.product_tmpl_id: self.product_uom_id = self.product_tmpl_id.uom_id.id @api.onchange('routing_id') def onchange_routing_id(self): for line in self.bom_line_ids: line.operation_id = False @api.multi def name_get(self): return [(bom.id, '%s%s' % (bom.code and '%s: ' % bom.code or '', bom.product_tmpl_id.display_name)) for bom in self] @api.multi def unlink(self): if self.env['mrp.production'].search( [('bom_id', 'in', self.ids), ('state', 'not in', ['done', 'cancel'])], limit=1): raise UserError( _('You can not delete a Bill of Material with running manufacturing orders.\nPlease close or cancel it first.' )) return super(MrpBom, self).unlink() @api.model def _bom_find(self, product_tmpl=None, product=None, picking_type=None, company_id=False): """ Finds BoM for particular product, picking and company """ if product: if not product_tmpl: product_tmpl = product.product_tmpl_id domain = [ '|', ('product_id', '=', product.id), '&', ('product_id', '=', False), ('product_tmpl_id', '=', product_tmpl.id) ] elif product_tmpl: domain = [('product_tmpl_id', '=', product_tmpl.id)] else: # neither product nor template, makes no sense to search return False if picking_type: domain += [ '|', ('picking_type_id', '=', picking_type.id), ('picking_type_id', '=', False) ] if company_id or self.env.context.get('company_id'): domain = domain + [('company_id', '=', company_id or self.env.context.get('company_id'))] # order to prioritize bom with product_id over the one without return self.search(domain, order='sequence, product_id', limit=1) def explode(self, product, quantity, picking_type=False): """ Explodes the BoM and creates two lists with all the information you need: bom_done and line_done Quantity describes the number of times you need the BoM: so the quantity divided by the number created by the BoM and converted into its UoM """ from collections import defaultdict graph = defaultdict(list) V = set() def check_cycle(v, visited, recStack, graph): visited[v] = True recStack[v] = True for neighbour in graph[v]: if visited[neighbour] == False: if check_cycle(neighbour, visited, recStack, graph) == True: return True elif recStack[neighbour] == True: return True recStack[v] = False return False boms_done = [(self, { 'qty': quantity, 'product': product, 'original_qty': quantity, 'parent_line': False })] lines_done = [] V |= set([product.product_tmpl_id.id]) bom_lines = [(bom_line, product, quantity, False) for bom_line in self.bom_line_ids] for bom_line in self.bom_line_ids: V |= set([bom_line.product_id.product_tmpl_id.id]) graph[product.product_tmpl_id.id].append( bom_line.product_id.product_tmpl_id.id) while bom_lines: current_line, current_product, current_qty, parent_line = bom_lines[ 0] bom_lines = bom_lines[1:] if current_line._skip_bom_line(current_product): continue line_quantity = current_qty * current_line.product_qty bom = self._bom_find(product=current_line.product_id, picking_type=picking_type or self.picking_type_id, company_id=self.company_id.id) if bom.type == 'phantom': converted_line_quantity = current_line.product_uom_id._compute_quantity( line_quantity / bom.product_qty, bom.product_uom_id) bom_lines = [(line, current_line.product_id, converted_line_quantity, current_line) for line in bom.bom_line_ids] + bom_lines for bom_line in bom.bom_line_ids: graph[current_line.product_id.product_tmpl_id.id].append( bom_line.product_id.product_tmpl_id.id) if bom_line.product_id.product_tmpl_id.id in V and check_cycle( bom_line.product_id.product_tmpl_id.id, {key: False for key in V}, {key: False for key in V}, graph): raise UserError( _('Recursion error! A product with a Bill of Material should not have itself in its BoM or child BoMs!' )) V |= set([bom_line.product_id.product_tmpl_id.id]) boms_done.append((bom, { 'qty': converted_line_quantity, 'product': current_product, 'original_qty': quantity, 'parent_line': current_line })) else: # We round up here because the user expects that if he has to consume a little more, the whole UOM unit # should be consumed. rounding = current_line.product_uom_id.rounding line_quantity = float_round(line_quantity, precision_rounding=rounding, rounding_method='UP') lines_done.append((current_line, { 'qty': line_quantity, 'product': current_product, 'original_qty': quantity, 'parent_line': parent_line })) return boms_done, lines_done
class PosConfig(models.Model): _name = 'pos.config' def _default_sale_journal(self): journal = self.env.ref('point_of_sale.pos_sale_journal', raise_if_not_found=False) if journal and journal.sudo().company_id == self.env.user.company_id: return journal return self._default_invoice_journal() def _default_invoice_journal(self): return self.env['account.journal'].search( [('type', '=', 'sale'), ('company_id', '=', self.env.user.company_id.id)], limit=1) def _default_pricelist(self): return self.env['product.pricelist'].search( [('currency_id', '=', self.env.user.company_id.currency_id.id)], limit=1) def _get_default_location(self): return self.env['stock.warehouse'].search( [('company_id', '=', self.env.user.company_id.id)], limit=1).lot_stock_id def _get_group_pos_manager(self): return self.env.ref('point_of_sale.group_pos_manager') def _get_group_pos_user(self): return self.env.ref('point_of_sale.group_pos_user') def _compute_default_customer_html(self): return self.env['ir.qweb'].render( 'point_of_sale.customer_facing_display_html') name = fields.Char(string='Point of Sale Name', index=True, required=True, help="An internal identification of the point of sale.") is_installed_account_accountant = fields.Boolean( compute="_compute_is_installed_account_accountant") journal_ids = fields.Many2many( 'account.journal', 'pos_config_journal_rel', 'pos_config_id', 'journal_id', string='Available Payment Methods', domain= "[('journal_user', '=', True ), ('type', 'in', ['bank', 'cash'])]", ) picking_type_id = fields.Many2one('stock.picking.type', string='Operation Type') use_existing_lots = fields.Boolean( related='picking_type_id.use_existing_lots') stock_location_id = fields.Many2one('stock.location', string='Stock Location', domain=[('usage', '=', 'internal')], required=True, default=_get_default_location) journal_id = fields.Many2one( 'account.journal', string='Sales Journal', domain=[('type', '=', 'sale')], help="Accounting journal used to post sales entries.", default=_default_sale_journal) invoice_journal_id = fields.Many2one( 'account.journal', string='Invoice Journal', domain=[('type', '=', 'sale')], help="Accounting journal used to create invoices.", default=_default_invoice_journal) currency_id = fields.Many2one('res.currency', compute='_compute_currency', string="Currency") iface_cashdrawer = fields.Boolean( string='Cashdrawer', help="Automatically open the cashdrawer.") iface_payment_terminal = fields.Boolean( string='Payment Terminal', help="Enables Payment Terminal integration.") iface_electronic_scale = fields.Boolean( string='Electronic Scale', help="Enables Electronic Scale integration.") iface_vkeyboard = fields.Boolean( string='Virtual KeyBoard', help= u"Don’t turn this option on if you take orders on smartphones or tablets. \n Such devices already benefit from a native keyboard." ) iface_customer_facing_display = fields.Boolean( string='Customer Facing Display', help="Show checkout to customers with a remotely-connected screen.") iface_print_via_proxy = fields.Boolean( string='Print via Proxy', help="Bypass browser printing and prints via the hardware proxy.") iface_scan_via_proxy = fields.Boolean( string='Scan via Proxy', help= "Enable barcode scanning with a remotely connected barcode scanner.") iface_invoicing = fields.Boolean( string='Invoicing', help='Enables invoice generation from the Point of Sale.') iface_big_scrollbars = fields.Boolean( 'Large Scrollbars', help='For imprecise industrial touchscreens.') iface_print_auto = fields.Boolean( string='Automatic Receipt Printing', default=False, help= 'The receipt will automatically be printed at the end of each order.') iface_print_skip_screen = fields.Boolean( string='Skip Preview Screen', default=True, help= 'The receipt screen will be skipped if the receipt can be printed automatically.' ) iface_precompute_cash = fields.Boolean( string='Prefill Cash Payment', help= 'The payment input will behave similarily to bank payment input, and will be prefilled with the exact due amount.' ) iface_tax_included = fields.Selection([('subtotal', 'Tax-Excluded Prices'), ('total', 'Tax-Included Prices')], "Tax Display", default='subtotal', required=True) iface_start_categ_id = fields.Many2one( 'pos.category', string='Initial Category', help= 'The point of sale will display this product category by default. If no category is specified, all available products will be shown.' ) iface_display_categ_images = fields.Boolean( string='Display Category Pictures', help="The product categories will be displayed with pictures.") restrict_price_control = fields.Boolean( string='Restrict Price Modifications to Managers', help= "Only users with Manager access rights for PoS app can modify the product prices on orders." ) cash_control = fields.Boolean( string='Cash Control', help="Check the amount of the cashbox at opening and closing.") receipt_header = fields.Text( string='Receipt Header', help= "A short text that will be inserted as a header in the printed receipt." ) receipt_footer = fields.Text( string='Receipt Footer', help= "A short text that will be inserted as a footer in the printed receipt." ) proxy_ip = fields.Char( string='IP Address', size=45, help= 'The hostname or ip address of the hardware proxy, Will be autodetected if left empty.' ) active = fields.Boolean(default=True) uuid = fields.Char( readonly=True, default=lambda self: str(uuid.uuid4()), help= 'A globally unique identifier for this pos configuration, used to prevent conflicts in client-generated data.' ) sequence_id = fields.Many2one( 'ir.sequence', string='Order IDs Sequence', readonly=True, help= "This sequence is automatically created by actpy but you can change it " "to customize the reference numbers of your orders.", copy=False) sequence_line_id = fields.Many2one( 'ir.sequence', string='Order Line IDs Sequence', readonly=True, help= "This sequence is automatically created by actpy but you can change it " "to customize the reference numbers of your orders lines.", copy=False) session_ids = fields.One2many('pos.session', 'config_id', string='Sessions') current_session_id = fields.Many2one('pos.session', compute='_compute_current_session', string="Current Session") current_session_state = fields.Char(compute='_compute_current_session') last_session_closing_cash = fields.Float(compute='_compute_last_session') last_session_closing_date = fields.Date(compute='_compute_last_session') pos_session_username = fields.Char(compute='_compute_current_session_user') pos_session_state = fields.Char(compute='_compute_current_session_user') group_by = fields.Boolean( string='Group Journal Items', default=True, help= "Check this if you want to group the Journal Items by Product while closing a Session." ) pricelist_id = fields.Many2one( 'product.pricelist', string='Default Pricelist', required=True, default=_default_pricelist, help= "The pricelist used if no customer is selected or if the customer has no Sale Pricelist configured." ) available_pricelist_ids = fields.Many2many( 'product.pricelist', string='Available Pricelists', default=_default_pricelist, help= "Make several pricelists available in the Point of Sale. You can also apply a pricelist to specific customers from their contact form (in Sales tab). To be valid, this pricelist must be listed here as an available pricelist. Otherwise the default pricelist will apply." ) company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.user.company_id) barcode_nomenclature_id = fields.Many2one( 'barcode.nomenclature', string='Barcode Nomenclature', help= 'Defines what kind of barcodes are available and how they are assigned to products, customers and cashiers.' ) group_pos_manager_id = fields.Many2one( 'res.groups', string='Point of Sale Manager Group', default=_get_group_pos_manager, help= 'This field is there to pass the id of the pos manager group to the point of sale client.' ) group_pos_user_id = fields.Many2one( 'res.groups', string='Point of Sale User Group', default=_get_group_pos_user, help= 'This field is there to pass the id of the pos user group to the point of sale client.' ) iface_tipproduct = fields.Boolean(string="Product tips") tip_product_id = fields.Many2one( 'product.product', string='Tip Product', help="This product is used as reference on customer receipts.") fiscal_position_ids = fields.Many2many( 'account.fiscal.position', string='Fiscal Positions', help= 'This is useful for restaurants with onsite and take-away services that imply specific tax rates.' ) default_fiscal_position_id = fields.Many2one( 'account.fiscal.position', string='Default Fiscal Position') default_cashbox_lines_ids = fields.One2many('account.cashbox.line', 'default_pos_id', string='Default Balance') customer_facing_display_html = fields.Html( string='Customer facing display content', translate=True, default=_compute_default_customer_html) use_pricelist = fields.Boolean("Use a pricelist.") group_sale_pricelist = fields.Boolean( "Use pricelists to adapt your price per customers", implied_group='product.group_sale_pricelist', help= """Allows to manage different prices based on rules per category of customers. Example: 10% for retailers, promotion of 5 EUR on this product, etc.""" ) group_pricelist_item = fields.Boolean( "Show pricelists to customers", implied_group='product.group_pricelist_item') tax_regime = fields.Boolean("Tax Regime") tax_regime_selection = fields.Boolean("Tax Regime Selection value") barcode_scanner = fields.Boolean("Barcode Scanner") start_category = fields.Boolean("Set Start Category") module_pos_restaurant = fields.Boolean("Is a Bar/Restaurant") module_pos_discount = fields.Boolean("Global Discounts") module_pos_mercury = fields.Boolean(string="Integrated Card Payments") module_pos_reprint = fields.Boolean(string="Reprint Receipt") is_posbox = fields.Boolean("PosBox") is_header_or_footer = fields.Boolean("Header & Footer") def _compute_is_installed_account_accountant(self): account_accountant = self.env['ir.module.module'].sudo().search([ ('name', '=', 'account_accountant'), ('state', '=', 'installed') ]) for pos_config in self: pos_config.is_installed_account_accountant = account_accountant and account_accountant.id @api.depends('journal_id.currency_id', 'journal_id.company_id.currency_id') def _compute_currency(self): for pos_config in self: if pos_config.journal_id: pos_config.currency_id = pos_config.journal_id.currency_id.id or pos_config.journal_id.company_id.currency_id.id else: pos_config.currency_id = self.env.user.company_id.currency_id.id @api.depends('session_ids') def _compute_current_session(self): for pos_config in self: session = pos_config.session_ids.filtered(lambda r: r.user_id.id == self.env.uid and \ not r.state == 'closed' and \ not r.rescue) # sessions ordered by id desc pos_config.current_session_id = session and session[0].id or False pos_config.current_session_state = session and session[ 0].state or False @api.depends('session_ids') def _compute_last_session(self): PosSession = self.env['pos.session'] for pos_config in self: session = PosSession.search_read( [('config_id', '=', pos_config.id), ('state', '=', 'closed')], ['cash_register_balance_end_real', 'stop_at'], order="stop_at desc", limit=1) if session: pos_config.last_session_closing_cash = session[0][ 'cash_register_balance_end_real'] pos_config.last_session_closing_date = session[0]['stop_at'] else: pos_config.last_session_closing_cash = 0 pos_config.last_session_closing_date = False @api.depends('session_ids') def _compute_current_session_user(self): for pos_config in self: session = pos_config.session_ids.filtered(lambda s: s.state in [ 'opening_control', 'opened', 'closing_control' ] and not s.rescue) pos_config.pos_session_username = session and session[ 0].user_id.name or False pos_config.pos_session_state = session and session[0].state or False @api.constrains('company_id', 'stock_location_id') def _check_company_location(self): if self.stock_location_id.company_id and self.stock_location_id.company_id.id != self.company_id.id: raise ValidationError( _("The company of the stock location is different than the one of point of sale" )) @api.constrains('company_id', 'journal_id') def _check_company_journal(self): if self.journal_id and self.journal_id.company_id.id != self.company_id.id: raise ValidationError( _("The company of the sales journal is different than the one of point of sale" )) @api.constrains('company_id', 'invoice_journal_id') def _check_company_invoice_journal(self): if self.invoice_journal_id and self.invoice_journal_id.company_id.id != self.company_id.id: raise ValidationError( _("The invoice journal and the point of sale must belong to the same company" )) @api.constrains('company_id', 'journal_ids') def _check_company_payment(self): if self.env['account.journal'].search_count([ ('id', 'in', self.journal_ids.ids), ('company_id', '!=', self.company_id.id) ]): raise ValidationError( _("The company of a payment method is different than the one of point of sale" )) @api.constrains('pricelist_id', 'available_pricelist_ids', 'journal_id', 'invoice_journal_id', 'journal_ids') def _check_currencies(self): if self.pricelist_id not in self.available_pricelist_ids: raise ValidationError( _("The default pricelist must be included in the available pricelists." )) if any( self.available_pricelist_ids.mapped( lambda pricelist: pricelist.currency_id != self.currency_id )): raise ValidationError( _("All available pricelists must be in the same currency as the company or" " as the Sales Journal set on this point of sale if you use" " the Accounting application.")) if self.invoice_journal_id.currency_id and self.invoice_journal_id.currency_id != self.currency_id: raise ValidationError( _("The invoice journal must be in the same currency as the Sales Journal or the company currency if that is not set." )) if any( self.journal_ids.mapped( lambda journal: journal.currency_id and journal.currency_id != self.currency_id)): raise ValidationError( _("All payment methods must be in the same currency as the Sales Journal or the company currency if that is not set." )) @api.onchange('iface_print_via_proxy') def _onchange_iface_print_via_proxy(self): self.iface_print_auto = self.iface_print_via_proxy @api.onchange('picking_type_id') def _onchange_picking_type_id(self): if self.picking_type_id.default_location_src_id.usage == 'internal' and self.picking_type_id.default_location_dest_id.usage == 'customer': self.stock_location_id = self.picking_type_id.default_location_src_id.id @api.onchange('use_pricelist') def _onchange_use_pricelist(self): """ If the 'pricelist' box is unchecked, we reset the pricelist_id to stop using a pricelist for this posbox. """ if not self.use_pricelist: self.pricelist_id = self._default_pricelist() else: self.update({ 'group_sale_pricelist': True, 'group_pricelist_item': True, }) @api.onchange('available_pricelist_ids') def _onchange_available_pricelist_ids(self): if self.pricelist_id not in self.available_pricelist_ids: self.pricelist_id = False @api.onchange('iface_scan_via_proxy') def _onchange_iface_scan_via_proxy(self): if self.iface_scan_via_proxy: self.barcode_scanner = True else: self.barcode_scanner = False @api.onchange('barcode_scanner') def _onchange_barcode_scanner(self): if self.barcode_scanner: self.barcode_nomenclature_id = self.env[ 'barcode.nomenclature'].search([], limit=1) else: self.barcode_nomenclature_id = False @api.onchange('is_posbox') def _onchange_is_posbox(self): if not self.is_posbox: self.proxy_ip = False self.iface_scan_via_proxy = False self.iface_electronic_scale = False self.iface_cashdrawer = False self.iface_print_via_proxy = False self.iface_customer_facing_display = False @api.onchange('tax_regime') def _onchange_tax_regime(self): if not self.tax_regime: self.default_fiscal_position_id = False @api.onchange('tax_regime_selection') def _onchange_tax_regime_selection(self): if not self.tax_regime_selection: self.fiscal_position_ids = [(5, 0, 0)] @api.onchange('start_category') def _onchange_start_category(self): if not self.start_category: self.iface_start_categ_id = False @api.onchange('is_header_or_footer') def _onchange_header_footer(self): if not self.is_header_or_footer: self.receipt_header = False self.receipt_footer = False @api.multi def name_get(self): result = [] for config in self: if (not config.session_ids) or (config.session_ids[0].state == 'closed'): result.append( (config.id, config.name + ' (' + _('not used') + ')')) continue result.append((config.id, config.name + ' (' + config.session_ids[0].user_id.name + ')')) return result @api.model def create(self, values): if values.get('is_posbox') and values.get( 'iface_customer_facing_display'): if values.get('customer_facing_display_html') and not values[ 'customer_facing_display_html'].strip(): values[ 'customer_facing_display_html'] = self._compute_default_customer_html( ) IrSequence = self.env['ir.sequence'].sudo() val = { 'name': _('POS Order %s') % values['name'], 'padding': 4, 'prefix': "%s/" % values['name'], 'code': "pos.order", 'company_id': values.get('company_id', False), } # force sequence_id field to new pos.order sequence values['sequence_id'] = IrSequence.create(val).id val.update(name=_('POS order line %s') % values['name'], code='pos.order.line') values['sequence_line_id'] = IrSequence.create(val).id pos_config = super(PosConfig, self).create(values) pos_config.sudo()._check_modules_to_install() pos_config.sudo()._check_groups_implied() # If you plan to add something after this, use a new environment. The one above is no longer valid after the modules install. return pos_config @api.multi def write(self, vals): if (self.is_posbox or vals.get('is_posbox')) and ( self.iface_customer_facing_display or vals.get('iface_customer_facing_display')): facing_display = (self.customer_facing_display_html or vals.get('customer_facing_display_html') or '').strip() if not facing_display: vals[ 'customer_facing_display_html'] = self._compute_default_customer_html( ) result = super(PosConfig, self).write(vals) self.sudo()._set_fiscal_position() self.sudo()._check_modules_to_install() self.sudo()._check_groups_implied() return result @api.multi def unlink(self): for pos_config in self.filtered( lambda pos_config: pos_config.sequence_id or pos_config. sequence_line_id): pos_config.sequence_id.unlink() pos_config.sequence_line_id.unlink() return super(PosConfig, self).unlink() def _set_fiscal_position(self): for config in self: if config.tax_regime and config.default_fiscal_position_id.id not in config.fiscal_position_ids.ids: config.fiscal_position_ids = [ (4, config.default_fiscal_position_id.id) ] elif not config.tax_regime_selection and not config.tax_regime and config.fiscal_position_ids.ids: config.fiscal_position_ids = [(5, 0, 0)] def _check_modules_to_install(self): module_installed = False for pos_config in self: for field_name in [ f for f in pos_config.fields_get_keys() if f.startswith('module_') ]: module_name = field_name.split('module_')[1] module_to_install = self.env['ir.module.module'].sudo().search( [('name', '=', module_name)]) if getattr(pos_config, field_name) and module_to_install.state not in ( 'installed', 'to install', 'to upgrade'): module_to_install.button_immediate_install() module_installed = True # just in case we want to do something if we install a module. (like a refresh ...) return module_installed def _check_groups_implied(self): for pos_config in self: for field_name in [ f for f in pos_config.fields_get_keys() if f.startswith('group_') ]: field = pos_config._fields[field_name] if field.type in ('boolean', 'selection') and hasattr( field, 'implied_group'): field_group_xmlids = getattr(field, 'group', 'base.group_user').split(',') field_groups = self.env['res.groups'].concat( *(self.env.ref(it) for it in field_group_xmlids)) field_groups.write({ 'implied_ids': [(4, self.env.ref(field.implied_group).id)] }) def execute(self): return { 'type': 'ir.actions.client', 'tag': 'reload', 'params': { 'wait': True } } # Methods to open the POS @api.multi def open_ui(self): """ open the pos interface """ self.ensure_one() return { 'type': 'ir.actions.act_url', 'url': '/pos/web/', 'target': 'self', } @api.multi def open_session_cb(self): """ new session button create one if none exist access cash control interface if enabled or start a session """ self.ensure_one() if not self.current_session_id: self.current_session_id = self.env['pos.session'].create({ 'user_id': self.env.uid, 'config_id': self.id }) if self.current_session_id.state == 'opened': return self.open_ui() return self._open_session(self.current_session_id.id) return self._open_session(self.current_session_id.id) @api.multi def open_existing_session_cb(self): """ close session button access session form to validate entries """ self.ensure_one() return self._open_session(self.current_session_id.id) def _open_session(self, session_id): return { 'name': _('Session'), 'view_type': 'form', 'view_mode': 'form,tree', 'res_model': 'pos.session', 'res_id': session_id, 'view_id': False, 'type': 'ir.actions.act_window', }
class Page(models.Model): _name = 'website.page' _inherits = {'ir.ui.view': 'view_id'} _inherit = 'website.published.mixin' _description = 'Page' url = fields.Char('Page URL') website_ids = fields.Many2many('website', string='Websites') view_id = fields.Many2one('ir.ui.view', string='View', required=True, ondelete="cascade") website_indexed = fields.Boolean('Page Indexed', default=True) date_publish = fields.Datetime('Publishing Date') # This is needed to be able to display if page is a menu in /website/pages menu_ids = fields.One2many('website.menu', 'page_id', 'Related Menus') is_homepage = fields.Boolean(compute='_compute_homepage', inverse='_set_homepage', string='Homepage') is_visible = fields.Boolean(compute='_compute_visible', string='Is Visible') @api.one def _compute_homepage(self): self.is_homepage = self == self.env['website'].get_current_website().homepage_id @api.one def _set_homepage(self): website = self.env['website'].get_current_website() if self.is_homepage: if website.homepage_id != self: website.write({'homepage_id': self.id}) else: if website.homepage_id == self: website.write({'homepage_id': None}) @api.one def _compute_visible(self): self.is_visible = self.website_published and (not self.date_publish or self.date_publish < fields.Datetime.now()) @api.model def get_page_info(self, id, website_id): domain = ['|', ('website_ids', 'in', website_id), ('website_ids', '=', False), ('id', '=', id)] item = self.search_read(domain, fields=['id', 'name', 'url', 'website_published', 'website_indexed', 'date_publish', 'menu_ids', 'is_homepage'], limit=1) return item @api.multi def get_view_identifier(self): """ Get identifier of this page view that may be used to render it """ return self.view_id.id @api.model def save_page_info(self, website_id, data): website = self.env['website'].browse(website_id) page = self.browse(int(data['id'])) #If URL has been edited, slug it original_url = page.url url = data['url'] if not url.startswith('/'): url = '/' + url if page.url != url: url = '/' + slugify(url, max_length=1024, path=True) url = self.env['website'].get_unique_path(url) #If name has changed, check for key uniqueness if page.name != data['name']: page_key = self.env['website'].get_unique_key(slugify(data['name'])) else: page_key = page.key menu = self.env['website.menu'].search([('page_id', '=', int(data['id']))]) if not data['is_menu']: #If the page is no longer in menu, we should remove its website_menu if menu: menu.unlink() else: #The page is now a menu, check if has already one if menu: menu.write({'url': url}) else: self.env['website.menu'].create({ 'name': data['name'], 'url': url, 'page_id': data['id'], 'parent_id': website.menu_id.id, 'website_id': website.id, }) page.write({ 'key': page_key, 'name': data['name'], 'url': url, 'website_published': data['website_published'], 'website_indexed': data['website_indexed'], 'date_publish': data['date_publish'] or None, 'is_homepage': data['is_homepage'], }) # Create redirect if needed if data['create_redirect']: self.env['website.redirect'].create({ 'type': data['redirect_type'], 'url_from': original_url, 'url_to': url, 'website_id': website.id, }) return url @api.multi def copy(self, default=None): view = self.env['ir.ui.view'].browse(self.view_id.id) # website.page's ir.ui.view should have a different key than the one it # is copied from. # (eg: website_version: an ir.ui.view record with the same key is # expected to be the same ir.ui.view but from another version) new_view = view.copy({'key': view.key + '.copy', 'name': '%s %s' % (view.name, _('(copy)'))}) default = { 'name': '%s %s' % (self.name, _('(copy)')), 'url': self.env['website'].get_unique_path(self.url), 'view_id': new_view.id, } return super(Page, self).copy(default=default) @api.model def clone_page(self, page_id, clone_menu=True): """ Clone a page, given its identifier :param page_id : website.page identifier """ page = self.browse(int(page_id)) new_page = page.copy() if clone_menu: menu = self.env['website.menu'].search([('page_id', '=', page_id)], limit=1) if menu: # If the page being cloned has a menu, clone it too new_menu = menu.copy() new_menu.write({'url': new_page.url, 'name': '%s %s' % (menu.name, _('(copy)')), 'page_id': new_page.id}) return new_page.url + '?enable_editor=1' @api.multi def unlink(self): """ When a website_page is deleted, the ORM does not delete its ir_ui_view. So we got to delete it ourself, but only if the ir_ui_view is not used by another website_page. """ # Handle it's ir_ui_view for page in self: # Other pages linked to the ir_ui_view of the page being deleted (will it even be possible?) pages_linked_to_iruiview = self.search( [('view_id', '=', self.view_id.id), ('id', '!=', self.id)] ) if len(pages_linked_to_iruiview) == 0: # If there is no other pages linked to that ir_ui_view, we can delete the ir_ui_view self.env['ir.ui.view'].search([('id', '=', self.view_id.id)]).unlink() # And then delete the website_page itself return super(Page, self).unlink() @api.model def delete_page(self, page_id): """ Delete a page, given its identifier :param page_id : website.page identifier """ # If we are deleting a page (that could possibly be a menu with a page) page = self.env['website.page'].browse(int(page_id)) if page: # Check if it is a menu with a page and also delete menu if so menu = self.env['website.menu'].search([('page_id', '=', page.id)], limit=1) if menu: menu.unlink() page.unlink() @api.multi def write(self, vals): self.ensure_one() if 'url' in vals and not vals['url'].startswith('/'): vals['url'] = '/' + vals['url'] result = super(Page, self).write(vals) return result
class EventTicket(models.Model): _name = 'event.event.ticket' _description = 'Event Ticket' def _default_product_id(self): return self.env.ref('event_sale.product_product_event', raise_if_not_found=False) name = fields.Char(string='Name', required=True, translate=True) event_type_id = fields.Many2one('event.type', string='Event Category', ondelete='cascade') event_id = fields.Many2one('event.event', string="Event", ondelete='cascade') product_id = fields.Many2one('product.product', string='Product', required=True, domain=[("event_ok", "=", True)], default=_default_product_id) registration_ids = fields.One2many('event.registration', 'event_ticket_id', string='Registrations') price = fields.Float(string='Price', digits=dp.get_precision('Product Price')) deadline = fields.Date(string="Sales End") is_expired = fields.Boolean(string='Is Expired', compute='_compute_is_expired') price_reduce = fields.Float(string="Price Reduce", compute="_compute_price_reduce", digits=dp.get_precision('Product Price')) price_reduce_taxinc = fields.Float(compute='_get_price_reduce_tax', string='Price Reduce Tax inc') # seats fields seats_availability = fields.Selection([('limited', 'Limited'), ('unlimited', 'Unlimited')], string='Available Seat', required=True, store=True, compute='_compute_seats', default="limited") seats_max = fields.Integer( string='Maximum Available Seats', help= "Define the number of available tickets. If you have too much registrations you will " "not be able to sell tickets anymore. Set 0 to ignore this rule set as unlimited." ) seats_reserved = fields.Integer(string='Reserved Seats', compute='_compute_seats', store=True) seats_available = fields.Integer(string='Available Seats', compute='_compute_seats', store=True) seats_unconfirmed = fields.Integer(string='Unconfirmed Seat Reservations', compute='_compute_seats', store=True) seats_used = fields.Integer(compute='_compute_seats', store=True) @api.multi def _compute_is_expired(self): for record in self: if record.deadline: current_date = fields.Date.context_today( record.with_context({'tz': record.event_id.date_tz})) record.is_expired = record.deadline < current_date else: record.is_expired = False @api.multi def _compute_price_reduce(self): for record in self: product = record.product_id discount = product.lst_price and ( product.lst_price - product.price) / product.lst_price or 0.0 record.price_reduce = (1.0 - discount) * record.price def _get_price_reduce_tax(self): for record in self: # sudo necessary here since the field is most probably accessed through the website tax_ids = record.sudo().product_id.taxes_id.filtered( lambda r: r.company_id == record.event_id.company_id) taxes = tax_ids.compute_all(record.price_reduce, record.event_id.company_id.currency_id, 1.0, product=record.product_id) record.price_reduce_taxinc = taxes['total_included'] @api.multi @api.depends('seats_max', 'registration_ids.state') def _compute_seats(self): """ Determine reserved, available, reserved but unconfirmed and used seats. """ # initialize fields to 0 + compute seats availability for ticket in self: ticket.seats_availability = 'unlimited' if ticket.seats_max == 0 else 'limited' ticket.seats_unconfirmed = ticket.seats_reserved = ticket.seats_used = ticket.seats_available = 0 # aggregate registrations by ticket and by state if self.ids: state_field = { 'draft': 'seats_unconfirmed', 'open': 'seats_reserved', 'done': 'seats_used', } query = """ SELECT event_ticket_id, state, count(event_id) FROM event_registration WHERE event_ticket_id IN %s AND state IN ('draft', 'open', 'done') GROUP BY event_ticket_id, state """ self.env.cr.execute(query, (tuple(self.ids), )) for event_ticket_id, state, num in self.env.cr.fetchall(): ticket = self.browse(event_ticket_id) ticket[state_field[state]] += num # compute seats_available for ticket in self: if ticket.seats_max > 0: ticket.seats_available = ticket.seats_max - ( ticket.seats_reserved + ticket.seats_used) @api.multi @api.constrains('registration_ids', 'seats_max') def _check_seats_limit(self): for record in self: if record.seats_max and record.seats_available < 0: raise ValidationError( _('No more available seats for the ticket')) @api.constrains('event_type_id', 'event_id') def _constrains_event(self): if any(ticket.event_type_id and ticket.event_id for ticket in self): raise UserError( _('Ticket should belong to either event category or event but not both' )) @api.onchange('product_id') def _onchange_product_id(self): self.price = self.product_id.list_price or 0
class MailMailStats(models.Model): """ MailMailStats models the statistics collected about emails. Those statistics are stored in a separated model and table to avoid bloating the mail_mail table with statistics values. This also allows to delete emails send with mass mailing without loosing the statistics about them. """ _name = 'mail.mail.statistics' _description = 'Email Statistics' _rec_name = 'message_id' _order = 'message_id' mail_mail_id = fields.Many2one('mail.mail', string='Mail', index=True) mail_mail_id_int = fields.Integer( string='Mail ID (tech)', help= 'ID of the related mail_mail. This field is an integer field because ' 'the related mail_mail can be deleted separately from its statistics. ' 'However the ID is needed for several action and controllers.', index=True, ) message_id = fields.Char(string='Message-ID') model = fields.Char(string='Document model') res_id = fields.Integer(string='Document ID') # campaign / wave data mass_mailing_id = fields.Many2one('mail.mass_mailing', string='Mass Mailing') mass_mailing_campaign_id = fields.Many2one( related='mass_mailing_id.mass_mailing_campaign_id', string='Mass Mailing Campaign', store=True, readonly=True) # Bounce and tracking scheduled = fields.Datetime(help='Date when the email has been created', default=fields.Datetime.now) sent = fields.Datetime(help='Date when the email has been sent') exception = fields.Datetime( help='Date of technical error leading to the email not being sent') opened = fields.Datetime( help='Date when the email has been opened the first time') replied = fields.Datetime( help='Date when this email has been replied for the first time.') bounced = fields.Datetime(help='Date when this email has bounced.') # Link tracking links_click_ids = fields.One2many('link.tracker.click', 'mail_stat_id', string='Links click') clicked = fields.Datetime( help='Date when customer clicked on at least one tracked link') # Status state = fields.Selection(compute="_compute_state", selection=[('outgoing', 'Outgoing'), ('exception', 'Exception'), ('sent', 'Sent'), ('opened', 'Opened'), ('replied', 'Replied'), ('bounced', 'Bounced')], store=True) state_update = fields.Datetime(compute="_compute_state", string='State Update', help='Last state update of the mail', store=True) recipient = fields.Char(compute="_compute_recipient") @api.depends('sent', 'opened', 'clicked', 'replied', 'bounced', 'exception') def _compute_state(self): self.update({'state_update': fields.Datetime.now()}) for stat in self: if stat.exception: stat.state = 'exception' elif stat.sent: stat.state = 'sent' elif stat.opened or stat.clicked: stat.state = 'opened' elif stat.replied: stat.state = 'replied' elif stat.bounced: stat.state = 'bounced' else: stat.state = 'outgoing' def _compute_recipient(self): for stat in self: if stat.model not in self.env: continue target = self.env[stat.model].browse(stat.res_id) if not target or not target.exists(): continue email = '' for email_field in ('email', 'email_from'): if email_field in target and target[email_field]: email = ' <%s>' % target[email_field] break stat.recipient = '%s%s' % (target.display_name, email) @api.model def create(self, values): if 'mail_mail_id' in values: values['mail_mail_id_int'] = values['mail_mail_id'] res = super(MailMailStats, self).create(values) return res def _get_records(self, mail_mail_ids=None, mail_message_ids=None, domain=None): if not self.ids and mail_mail_ids: base_domain = [('mail_mail_id_int', 'in', mail_mail_ids)] elif not self.ids and mail_message_ids: base_domain = [('message_id', 'in', mail_message_ids)] else: base_domain = [('id', 'in', self.ids)] if domain: base_domain = ['&'] + domain + base_domain return self.search(base_domain) def set_opened(self, mail_mail_ids=None, mail_message_ids=None): statistics = self._get_records(mail_mail_ids, mail_message_ids, [('opened', '=', False)]) statistics.write({'opened': fields.Datetime.now()}) return statistics def set_clicked(self, mail_mail_ids=None, mail_message_ids=None): statistics = self._get_records(mail_mail_ids, mail_message_ids, [('clicked', '=', False)]) statistics.write({'clicked': fields.Datetime.now()}) return statistics def set_replied(self, mail_mail_ids=None, mail_message_ids=None): statistics = self._get_records(mail_mail_ids, mail_message_ids, [('replied', '=', False)]) statistics.write({'replied': fields.Datetime.now()}) return statistics def set_bounced(self, mail_mail_ids=None, mail_message_ids=None): statistics = self._get_records(mail_mail_ids, mail_message_ids, [('bounced', '=', False)]) statistics.write({'bounced': fields.Datetime.now()}) return statistics
class IrActionsActWindow(models.Model): _name = 'ir.actions.act_window' _table = 'ir_act_window' _inherit = 'ir.actions.actions' _sequence = 'ir_actions_id_seq' _order = 'name' @api.constrains('res_model', 'src_model') def _check_model(self): for action in self: if action.res_model not in self.env: raise ValidationError( _('Invalid model name %r in action definition.') % action.res_model) if action.src_model and action.src_model not in self.env: raise ValidationError( _('Invalid model name %r in action definition.') % action.src_model) @api.depends('view_ids.view_mode', 'view_mode', 'view_id.type') def _compute_views(self): """ Compute an ordered list of the specific view modes that should be enabled when displaying the result of this action, along with the ID of the specific view to use for each mode, if any were required. This function hides the logic of determining the precedence between the view_modes string, the view_ids o2m, and the view_id m2o that can be set on the action. """ for act in self: act.views = [(view.view_id.id, view.view_mode) for view in act.view_ids] got_modes = [view.view_mode for view in act.view_ids] all_modes = act.view_mode.split(',') missing_modes = [ mode for mode in all_modes if mode not in got_modes ] if missing_modes: if act.view_id.type in missing_modes: # reorder missing modes to put view_id first if present missing_modes.remove(act.view_id.type) act.views.append((act.view_id.id, act.view_id.type)) act.views.extend([(False, mode) for mode in missing_modes]) @api.depends('res_model', 'search_view_id') def _compute_search_view(self): for act in self: fvg = self.env[act.res_model].fields_view_get( act.search_view_id.id, 'search') act.search_view = str(fvg) name = fields.Char(string='Action Name', translate=True) type = fields.Char(default="ir.actions.act_window") view_id = fields.Many2one('ir.ui.view', string='View Ref.', ondelete='set null') domain = fields.Char( string='Domain Value', help= "Optional domain filtering of the destination data, as a Python expression" ) context = fields.Char( string='Context Value', default={}, required=True, help= "Context dictionary as Python expression, empty by default (Default: {})" ) res_id = fields.Integer( string='Record ID', help= "Database ID of record to open in form view, when ``view_mode`` is set to 'form' only" ) res_model = fields.Char( string='Destination Model', required=True, help="Model name of the object to open in the view window") src_model = fields.Char( string='Source Model', help= "Optional model name of the objects on which this action should be visible" ) target = fields.Selection([('current', 'Current Window'), ('new', 'New Window'), ('inline', 'Inline Edit'), ('fullscreen', 'Full Screen'), ('main', 'Main action of Current Window')], default="current", string='Target Window') view_mode = fields.Char( required=True, default='tree,form', help= "Comma-separated list of allowed view modes, such as 'form', 'tree', 'calendar', etc. (Default: tree,form)" ) view_type = fields.Selection( [('tree', 'Tree'), ('form', 'Form')], default="form", string='View Type', required=True, help= "View type: Tree type to use for the tree view, set to 'tree' for a hierarchical tree view, or 'form' for a regular list view" ) usage = fields.Char( string='Action Usage', help="Used to filter menu and home actions from the user form.") view_ids = fields.One2many('ir.actions.act_window.view', 'act_window_id', string='Views') views = fields.Binary(compute='_compute_views', help="This function field computes the ordered list of views that should be enabled " \ "when displaying the result of an action, federating view mode, views and " \ "reference view. The result is returned as an ordered list of pairs (view_id,view_mode).") limit = fields.Integer(default=80, help='Default limit for the list view') groups_id = fields.Many2many('res.groups', 'ir_act_window_group_rel', 'act_id', 'gid', string='Groups') search_view_id = fields.Many2one('ir.ui.view', string='Search View Ref.') filter = fields.Boolean() auto_search = fields.Boolean(default=True) search_view = fields.Text(compute='_compute_search_view') multi = fields.Boolean( string='Restrict to lists', help= "If checked and the action is bound to a model, it will only appear in the More menu on list views" ) @api.multi def read(self, fields=None, load='_classic_read'): """ call the method get_empty_list_help of the model and set the window action help message """ result = super(IrActionsActWindow, self).read(fields, load=load) if not fields or 'help' in fields: for values in result: model = values.get('res_model') if model in self.env: values['help'] = self.env[model].get_empty_list_help( values.get('help', "")) return result @api.model def for_xml_id(self, module, xml_id): """ Returns the act_window object created for the provided xml_id :param module: the module the act_window originates in :param xml_id: the namespace-less id of the action (the @id attribute from the XML file) :return: A read() view of the ir.actions.act_window """ record = self.env.ref("%s.%s" % (module, xml_id)) return record.read()[0] @api.model def create(self, vals): self.clear_caches() return super(IrActionsActWindow, self).create(vals) @api.multi def unlink(self): self.clear_caches() return super(IrActionsActWindow, self).unlink() @api.multi def exists(self): ids = self._existing() existing = self.filtered(lambda rec: rec.id in ids) if len(existing) < len(self): # mark missing records in cache with a failed value exc = MissingError(_("Record does not exist or has been deleted.")) for record in (self - existing): record._cache.set_failed(self._fields, exc) return existing @api.model @tools.ormcache() def _existing(self): self._cr.execute("SELECT id FROM %s" % self._table) return set(row[0] for row in self._cr.fetchall())
class StockMove(models.Model): _inherit = 'stock.move' created_production_id = fields.Many2one('mrp.production', 'Created Production Order') production_id = fields.Many2one('mrp.production', 'Production Order for finished products') raw_material_production_id = fields.Many2one( 'mrp.production', 'Production Order for raw materials') unbuild_id = fields.Many2one('mrp.unbuild', 'Disassembly Order') consume_unbuild_id = fields.Many2one('mrp.unbuild', 'Consumed Disassembly Order') operation_id = fields.Many2one('mrp.routing.workcenter', 'Operation To Consume') # TDE FIXME: naming workorder_id = fields.Many2one('mrp.workorder', 'Work Order To Consume') # Quantities to process, in normalized UoMs active_move_line_ids = fields.One2many('stock.move.line', 'move_id', domain=[('done_wo', '=', True)], string='Lots') bom_line_id = fields.Many2one('mrp.bom.line', 'BoM Line') unit_factor = fields.Float('Unit Factor') is_done = fields.Boolean('Done', compute='_compute_is_done', store=True, help='Technical Field to order moves') needs_lots = fields.Boolean('Tracking', compute='_compute_needs_lots') order_finished_lot_ids = fields.Many2many( 'stock.production.lot', compute='_compute_order_finished_lot_ids') finished_lots_exist = fields.Boolean( 'Finished Lots Exist', compute='_compute_order_finished_lot_ids') @api.depends('active_move_line_ids.qty_done', 'active_move_line_ids.product_uom_id') def _compute_done_quantity(self): super(StockMove, self)._compute_done_quantity() @api.depends( 'raw_material_production_id.move_finished_ids.move_line_ids.lot_id') def _compute_order_finished_lot_ids(self): for move in self: if move.raw_material_production_id.move_finished_ids: finished_lots_ids = move.raw_material_production_id.move_finished_ids.mapped( 'move_line_ids.lot_id').ids if finished_lots_ids: move.order_finished_lot_ids = finished_lots_ids move.finished_lots_exist = True else: move.finished_lots_exist = False @api.depends('product_id.tracking') def _compute_needs_lots(self): for move in self: move.needs_lots = move.product_id.tracking != 'none' @api.depends('raw_material_production_id.is_locked', 'picking_id.is_locked') def _compute_is_locked(self): super(StockMove, self)._compute_is_locked() for move in self: if move.raw_material_production_id: move.is_locked = move.raw_material_production_id.is_locked def _get_move_lines(self): self.ensure_one() if self.raw_material_production_id: return self.active_move_line_ids else: return super(StockMove, self)._get_move_lines() @api.depends('state') def _compute_is_done(self): for move in self: move.is_done = (move.state in ('done', 'cancel')) @api.model def default_get(self, fields_list): defaults = super(StockMove, self).default_get(fields_list) if self.env.context.get('default_raw_material_production_id'): production_id = self.env['mrp.production'].browse( self.env.context['default_raw_material_production_id']) if production_id.state == 'done': defaults['state'] = 'done' defaults['product_uom_qty'] = 0.0 defaults['additional'] = True return defaults def _action_assign(self): res = super(StockMove, self)._action_assign() for move in self.filtered( lambda x: x.production_id or x.raw_material_production_id): if move.move_line_ids: move.move_line_ids.write({ 'production_id': move.raw_material_production_id.id, 'workorder_id': move.workorder_id.id, }) return res def _action_cancel(self): if any(move.quantity_done and ( move.raw_material_production_id or move.production_id) for move in self): raise exceptions.UserError( _('You cannot cancel a manufacturing order if you have already consumed material.\ If you want to cancel this MO, please change the consumed quantities to 0.' )) return super(StockMove, self)._action_cancel() def _action_confirm(self, merge=True, merge_into=False): moves = self.env['stock.move'] for move in self: moves |= move.action_explode() # we go further with the list of ids potentially changed by action_explode return super(StockMove, moves)._action_confirm(merge=merge, merge_into=merge_into) def action_explode(self): """ Explodes pickings """ # in order to explode a move, we must have a picking_type_id on that move because otherwise the move # won't be assigned to a picking and it would be weird to explode a move into several if they aren't # all grouped in the same picking. if not self.picking_type_id: return self bom = self.env['mrp.bom'].sudo()._bom_find( product=self.product_id, company_id=self.company_id.id) if not bom or bom.type != 'phantom': return self phantom_moves = self.env['stock.move'] processed_moves = self.env['stock.move'] factor = self.product_uom._compute_quantity( self.product_uom_qty, bom.product_uom_id) / bom.product_qty boms, lines = bom.sudo().explode(self.product_id, factor, picking_type=bom.picking_type_id) for bom_line, line_data in lines: phantom_moves += self._generate_move_phantom( bom_line, line_data['qty']) for new_move in phantom_moves: processed_moves |= new_move.action_explode() # if not self.split_from and self.procurement_id: # # Check if procurements have been made to wait for # moves = self.procurement_id.move_ids # if len(moves) == 1: # self.procurement_id.write({'state': 'done'}) if processed_moves and self.state == 'assigned': # Set the state of resulting moves according to 'assigned' as the original move is assigned processed_moves.write({'state': 'assigned'}) # delete the move with original product which is not relevant anymore self.sudo().unlink() return processed_moves def _prepare_phantom_move_values(self, bom_line, quantity): return { 'picking_id': self.picking_id.id if self.picking_id else False, 'product_id': bom_line.product_id.id, 'product_uom': bom_line.product_uom_id.id, 'product_uom_qty': quantity, 'state': 'draft', # will be confirmed below 'name': self.name, } def _generate_move_phantom(self, bom_line, quantity): if bom_line.product_id.type in ['product', 'consu']: return self.copy( default=self._prepare_phantom_move_values(bom_line, quantity)) return self.env['stock.move'] def _generate_consumed_move_line(self, qty_to_add, final_lot, lot=False): if lot: move_lines = self.move_line_ids.filtered( lambda ml: ml.lot_id == lot and not ml.lot_produced_id) else: move_lines = self.move_line_ids.filtered( lambda ml: not ml.lot_id and not ml.lot_produced_id) # Sanity check: if the product is a serial number and `lot` is already present in the other # consumed move lines, raise. if lot and self.product_id.tracking == 'serial' and lot in self.move_line_ids.filtered( lambda ml: ml.qty_done).mapped('lot_id'): raise UserError( _('You cannot consume the same serial number twice. Please correct the serial numbers encoded.' )) for ml in move_lines: rounding = ml.product_uom_id.rounding if float_compare(qty_to_add, 0, precision_rounding=rounding) <= 0: break quantity_to_process = min(qty_to_add, ml.product_uom_qty - ml.qty_done) qty_to_add -= quantity_to_process new_quantity_done = (ml.qty_done + quantity_to_process) if float_compare(new_quantity_done, ml.product_uom_qty, precision_rounding=rounding) >= 0: ml.write({ 'qty_done': new_quantity_done, 'lot_produced_id': final_lot.id }) else: new_qty_reserved = ml.product_uom_qty - new_quantity_done default = { 'product_uom_qty': new_quantity_done, 'qty_done': new_quantity_done, 'lot_produced_id': final_lot.id } ml.copy(default=default) ml.with_context(bypass_reservation_update=True).write({ 'product_uom_qty': new_qty_reserved, 'qty_done': 0 }) if float_compare(qty_to_add, 0, precision_rounding=self.product_uom.rounding) > 0: # Search for a sub-location where the product is available. This might not be perfectly # correct if the quantity available is spread in several sub-locations, but at least # we should be closer to the reality. Anyway, no reservation is made, so it is still # possible to change it afterwards. quants = self.env['stock.quant']._gather(self.product_id, self.location_id, lot_id=lot, strict=False) available_quantity = self.product_id.uom_id._compute_quantity( self.env['stock.quant']._get_available_quantity( self.product_id, self.location_id, lot_id=lot, strict=False), self.product_uom) location_id = False if float_compare(qty_to_add, available_quantity, precision_rounding=self.product_uom.rounding) < 0: location_id = quants.filtered( lambda r: r.quantity > 0)[-1:].location_id vals = { 'move_id': self.id, 'product_id': self.product_id.id, 'location_id': location_id.id if location_id else self.location_id.id, 'location_dest_id': self.location_dest_id.id, 'product_uom_qty': 0, 'product_uom_id': self.product_uom.id, 'qty_done': qty_to_add, 'lot_produced_id': final_lot.id, } if lot: vals.update({'lot_id': lot.id}) self.env['stock.move.line'].create(vals)
class Module(models.Model): _name = "ir.module.module" _rec_name = "shortdesc" _description = "Module" _order = 'sequence,name' @api.model def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): res = super(Module, self).fields_view_get(view_id, view_type, toolbar=toolbar, submenu=False) if view_type == 'form' and res.get('toolbar',False): install_id = self.env.ref('base.action_server_module_immediate_install').id action = [rec for rec in res['toolbar']['action'] if rec.get('id', False) != install_id] res['toolbar'] = {'action': action} return res @classmethod def get_module_info(cls, name): try: return modules.load_information_from_description_file(name) except Exception: _logger.debug('Error when trying to fetch information for module %s', name, exc_info=True) return {} @api.depends('name', 'description') def _get_desc(self): for module in self: path = modules.get_module_resource(module.name, 'static/description/index.html') if path: with tools.file_open(path, 'rb') as desc_file: doc = desc_file.read() html = lxml.html.document_fromstring(doc) for element, attribute, link, pos in html.iterlinks(): if element.get('src') and not '//' in element.get('src') and not 'static/' in element.get('src'): element.set('src', "/%s/static/description/%s" % (module.name, element.get('src'))) module.description_html = tools.html_sanitize(lxml.html.tostring(html)) else: overrides = { 'embed_stylesheet': False, 'doctitle_xform': False, 'output_encoding': 'unicode', 'xml_declaration': False, } output = publish_string(source=module.description or '', settings_overrides=overrides, writer=MyWriter()) module.description_html = tools.html_sanitize(output) @api.depends('name') def _get_latest_version(self): default_version = modules.adapt_version('1.0') for module in self: module.installed_version = self.get_module_info(module.name).get('version', default_version) @api.depends('name', 'state') def _get_views(self): IrModelData = self.env['ir.model.data'].with_context(active_test=True) dmodels = ['ir.ui.view', 'ir.actions.report', 'ir.ui.menu'] for module in self: # Skip uninstalled modules below, no data to find anyway. if module.state not in ('installed', 'to upgrade', 'to remove'): module.views_by_module = "" module.reports_by_module = "" module.menus_by_module = "" continue # then, search and group ir.model.data records imd_models = defaultdict(list) imd_domain = [('module', '=', module.name), ('model', 'in', tuple(dmodels))] for data in IrModelData.sudo().search(imd_domain): imd_models[data.model].append(data.res_id) def browse(model): # as this method is called before the module update, some xmlid # may be invalid at this stage; explictly filter records before # reading them return self.env[model].browse(imd_models[model]).exists() def format_view(v): return '%s%s (%s)' % (v.inherit_id and '* INHERIT ' or '', v.name, v.type) module.views_by_module = "\n".join(sorted(format_view(v) for v in browse('ir.ui.view'))) module.reports_by_module = "\n".join(sorted(r.name for r in browse('ir.actions.report'))) module.menus_by_module = "\n".join(sorted(m.complete_name for m in browse('ir.ui.menu'))) @api.depends('icon') def _get_icon_image(self): for module in self: module.icon_image = '' if module.icon: path_parts = module.icon.split('/') path = modules.get_module_resource(path_parts[1], *path_parts[2:]) else: path = modules.module.get_module_icon(module.name) if path: with tools.file_open(path, 'rb') as image_file: module.icon_image = base64.b64encode(image_file.read()) name = fields.Char('Technical Name', readonly=True, required=True, index=True) category_id = fields.Many2one('ir.module.category', string='Category', readonly=True, index=True) shortdesc = fields.Char('Module Name', readonly=True, translate=True) summary = fields.Char('Summary', readonly=True, translate=True) description = fields.Text('Description', readonly=True, translate=True) description_html = fields.Html('Description HTML', compute='_get_desc') author = fields.Char("Author", readonly=True) maintainer = fields.Char('Maintainer', readonly=True) contributors = fields.Text('Contributors', readonly=True) website = fields.Char("Website", readonly=True) # attention: Incorrect field names !! # installed_version refers the latest version (the one on disk) # latest_version refers the installed version (the one in database) # published_version refers the version available on the repository installed_version = fields.Char('Latest Version', compute='_get_latest_version') latest_version = fields.Char('Installed Version', readonly=True) published_version = fields.Char('Published Version', readonly=True) url = fields.Char('URL', readonly=True) sequence = fields.Integer('Sequence', default=100) dependencies_id = fields.One2many('ir.module.module.dependency', 'module_id', string='Dependencies', readonly=True) exclusion_ids = fields.One2many('ir.module.module.exclusion', 'module_id', string='Exclusions', readonly=True) auto_install = fields.Boolean('Automatic Installation', help='An auto-installable module is automatically installed by the ' 'system when all its dependencies are satisfied. ' 'If the module has no dependency, it is always installed.') state = fields.Selection(STATES, string='Status', default='uninstalled', readonly=True, index=True) demo = fields.Boolean('Demo Data', default=False, readonly=True) license = fields.Selection([ ('GPL-2', 'GPL Version 2'), ('GPL-2 or any later version', 'GPL-2 or later version'), ('GPL-3', 'GPL Version 3'), ('GPL-3 or any later version', 'GPL-3 or later version'), ('AGPL-3', 'Affero GPL-3'), ('LGPL-3', 'LGPL Version 3'), ('Other OSI approved licence', 'Other OSI Approved Licence'), ('FEEL-1', 'actpy Enterprise Edition License v1.0'), ('FPL-1', 'actpy Proprietary License v1.0'), ('OPL-1', 'Odoo Proprietary License v1.0'), ('Other proprietary', 'Other Proprietary') ], string='License', default='LGPL-3', readonly=True) menus_by_module = fields.Text(string='Menus', compute='_get_views', store=True) reports_by_module = fields.Text(string='Reports', compute='_get_views', store=True) views_by_module = fields.Text(string='Views', compute='_get_views', store=True) application = fields.Boolean('Application', readonly=True) icon = fields.Char('Icon URL') icon_image = fields.Binary(string='Icon', compute='_get_icon_image') _sql_constraints = [ ('name_uniq', 'UNIQUE (name)', 'The name of the module must be unique!'), ] @api.multi def unlink(self): if not self: return True for module in self: if module.state in ('installed', 'to upgrade', 'to remove', 'to install'): raise UserError(_('You try to remove a module that is installed or will be installed')) self.clear_caches() # Installing a module creates entries in base.module.uninstall, during # the unlink process of ir.module.module we try to update the # base.module.uninstall table's module_id to null, which violates a # non-null constraint, effectively raising an Exception. # V11-only !!DO NOT FORWARD-PORT!! self.env['base.module.uninstall'].search( [('module_id', 'in', self.ids)] ).unlink() return super(Module, self).unlink() @staticmethod def _check_external_dependencies(terp): depends = terp.get('external_dependencies') if not depends: return for pydep in depends.get('python', []): try: importlib.import_module(pydep) except ImportError: raise ImportError('No module named %s' % (pydep,)) for binary in depends.get('bin', []): try: tools.find_in_path(binary) except IOError: raise Exception('Unable to find %r in path' % (binary,)) @classmethod def check_external_dependencies(cls, module_name, newstate='to install'): terp = cls.get_module_info(module_name) try: cls._check_external_dependencies(terp) except Exception as e: if newstate == 'to install': msg = _('Unable to install module "%s" because an external dependency is not met: %s') elif newstate == 'to upgrade': msg = _('Unable to upgrade module "%s" because an external dependency is not met: %s') else: msg = _('Unable to process module "%s" because an external dependency is not met: %s') raise UserError(msg % (module_name, e.args[0])) @api.multi def _state_update(self, newstate, states_to_update, level=100): if level < 1: raise UserError(_('Recursion error in modules dependencies !')) # whether some modules are installed with demo data demo = False for module in self: # determine dependency modules to update/others update_mods, ready_mods = self.browse(), self.browse() for dep in module.dependencies_id: if dep.state == 'unknown': raise UserError(_("You try to install module '%s' that depends on module '%s'.\nBut the latter module is not available in your system.") % (module.name, dep.name,)) if dep.depend_id.state == newstate: ready_mods += dep.depend_id else: update_mods += dep.depend_id # update dependency modules that require it, and determine demo for module update_demo = update_mods._state_update(newstate, states_to_update, level=level-1) module_demo = module.demo or update_demo or any(mod.demo for mod in ready_mods) demo = demo or module_demo # check dependencies and update module itself self.check_external_dependencies(module.name, newstate) if module.state in states_to_update: module.write({'state': newstate, 'demo': module_demo}) return demo @assert_log_admin_access @api.multi def button_install(self): # domain to select auto-installable (but not yet installed) modules auto_domain = [('state', '=', 'uninstalled'), ('auto_install', '=', True)] # determine whether an auto-install module must be installed: # - all its dependencies are installed or to be installed, # - at least one dependency is 'to install' install_states = frozenset(('installed', 'to install', 'to upgrade')) def must_install(module): states = set(dep.state for dep in module.dependencies_id) return states <= install_states and 'to install' in states modules = self while modules: # Mark the given modules and their dependencies to be installed. modules._state_update('to install', ['uninstalled']) # Determine which auto-installable modules must be installed. modules = self.search(auto_domain).filtered(must_install) # the modules that are installed/to install/to upgrade install_mods = self.search([('state', 'in', list(install_states))]) # check individual exclusions install_names = {module.name for module in install_mods} for module in install_mods: for exclusion in module.exclusion_ids: if exclusion.name in install_names: msg = _('Modules "%s" and "%s" are incompatible.') raise UserError(msg % (module.shortdesc, exclusion.exclusion_id.shortdesc)) # check category exclusions def closure(module): todo = result = module while todo: result |= todo todo = todo.mapped('dependencies_id.depend_id') return result exclusives = self.env['ir.module.category'].search([('exclusive', '=', True)]) for category in exclusives: # retrieve installed modules in category and sub-categories categories = category.search([('id', 'child_of', category.ids)]) modules = install_mods.filtered(lambda mod: mod.category_id in categories) # the installation is valid if all installed modules in categories # belong to the transitive dependencies of one of them if modules and not any(modules <= closure(module) for module in modules): msg = _('You are trying to install incompatible modules in category "%s":') labels = dict(self.fields_get(['state'])['state']['selection']) raise UserError("\n".join([msg % category.name] + [ "- %s (%s)" % (module.shortdesc, labels[module.state]) for module in modules ])) return dict(ACTION_DICT, name=_('Install')) @assert_log_admin_access @api.multi def button_immediate_install(self): """ Installs the selected module(s) immediately and fully, returns the next res.config action to execute :returns: next res.config item to execute :rtype: dict[str, object] """ _logger.info('User #%d triggered module installation', self.env.uid) return self._button_immediate_function(type(self).button_install) @assert_log_admin_access @api.multi def button_install_cancel(self): self.write({'state': 'uninstalled', 'demo': False}) return True @assert_log_admin_access @api.multi def module_uninstall(self): """ Perform the various steps required to uninstall a module completely including the deletion of all database structures created by the module: tables, columns, constraints, etc. """ modules_to_remove = self.mapped('name') self.env['ir.model.data']._module_data_uninstall(modules_to_remove) self.write({'state': 'uninstalled', 'latest_version': False}) return True @api.multi @api.returns('self') def downstream_dependencies(self, known_deps=None, exclude_states=('uninstalled', 'uninstallable', 'to remove')): """ Return the modules that directly or indirectly depend on the modules in `self`, and that satisfy the `exclude_states` filter. """ if not self: return self known_deps = known_deps or self.browse() query = """ SELECT DISTINCT m.id FROM ir_module_module_dependency d JOIN ir_module_module m ON (d.module_id=m.id) WHERE d.name IN (SELECT name from ir_module_module where id in %s) AND m.state NOT IN %s AND m.id NOT IN %s """ self._cr.execute(query, (tuple(self.ids), tuple(exclude_states), tuple(known_deps.ids or self.ids))) new_deps = self.browse([row[0] for row in self._cr.fetchall()]) missing_mods = new_deps - known_deps known_deps |= new_deps if missing_mods: known_deps |= missing_mods.downstream_dependencies(known_deps, exclude_states) return known_deps @api.multi @api.returns('self') def upstream_dependencies(self, known_deps=None, exclude_states=('installed', 'uninstallable', 'to remove')): """ Return the dependency tree of modules of the modules in `self`, and that satisfy the `exclude_states` filter. """ if not self: return self known_deps = known_deps or self.browse() query = """ SELECT DISTINCT m.id FROM ir_module_module_dependency d JOIN ir_module_module m ON (d.module_id=m.id) WHERE m.name IN (SELECT name from ir_module_module_dependency where module_id in %s) AND m.state NOT IN %s AND m.id NOT IN %s """ self._cr.execute(query, (tuple(self.ids), tuple(exclude_states), tuple(known_deps.ids or self.ids))) new_deps = self.browse([row[0] for row in self._cr.fetchall()]) missing_mods = new_deps - known_deps known_deps |= new_deps if missing_mods: known_deps |= missing_mods.upstream_dependencies(known_deps, exclude_states) return known_deps def next(self): """ Return the action linked to an ir.actions.todo is there exists one that should be executed. Otherwise, redirect to /web """ Todos = self.env['ir.actions.todo'] _logger.info('getting next %s', Todos) active_todo = Todos.search([('state', '=', 'open')], limit=1) if active_todo: _logger.info('next action is %s', active_todo) return active_todo.action_launch() return { 'type': 'ir.actions.act_url', 'target': 'self', 'url': '/web', } @api.multi def _button_immediate_function(self, function): function(self) self._cr.commit() api.Environment.reset() modules.registry.Registry.new(self._cr.dbname, update_module=True) self._cr.commit() env = api.Environment(self._cr, self._uid, self._context) # pylint: disable=next-method-called config = env['ir.module.module'].next() or {} if config.get('type') not in ('ir.actions.act_window_close',): return config # reload the client; open the first available root menu menu = env['ir.ui.menu'].search([('parent_id', '=', False)])[:1] return { 'type': 'ir.actions.client', 'tag': 'reload', 'params': {'menu_id': menu.id}, } @assert_log_admin_access @api.multi def button_immediate_uninstall(self): """ Uninstall the selected module(s) immediately and fully, returns the next res.config action to execute """ _logger.info('User #%d triggered module uninstallation', self.env.uid) return self._button_immediate_function(type(self).button_uninstall) @assert_log_admin_access @api.multi def button_uninstall(self): if 'base' in self.mapped('name'): raise UserError(_("The `base` module cannot be uninstalled")) deps = self.downstream_dependencies() (self + deps).write({'state': 'to remove'}) return dict(ACTION_DICT, name=_('Uninstall')) @assert_log_admin_access @api.multi def button_uninstall_wizard(self): """ Launch the wizard to uninstall the given module. """ return { 'type': 'ir.actions.act_window', 'target': 'new', 'name': _('Uninstall module'), 'view_mode': 'form', 'res_model': 'base.module.uninstall', 'context': {'default_module_id': self.id}, } @api.multi def button_uninstall_cancel(self): self.write({'state': 'installed'}) return True @assert_log_admin_access @api.multi def button_immediate_upgrade(self): """ Upgrade the selected module(s) immediately and fully, return the next res.config action to execute """ return self._button_immediate_function(type(self).button_upgrade) @assert_log_admin_access @api.multi def button_upgrade(self): Dependency = self.env['ir.module.module.dependency'] self.update_list() todo = list(self) i = 0 while i < len(todo): module = todo[i] i += 1 if module.state not in ('installed', 'to upgrade'): raise UserError(_("Can not upgrade module '%s'. It is not installed.") % (module.name,)) self.check_external_dependencies(module.name, 'to upgrade') for dep in Dependency.search([('name', '=', module.name)]): if dep.module_id.state == 'installed' and dep.module_id not in todo: todo.append(dep.module_id) self.browse(module.id for module in todo).write({'state': 'to upgrade'}) to_install = [] for module in todo: for dep in module.dependencies_id: if dep.state == 'unknown': raise UserError(_('You try to upgrade the module %s that depends on the module: %s.\nBut this module is not available in your system.') % (module.name, dep.name,)) if dep.state == 'uninstalled': to_install += self.search([('name', '=', dep.name)]).ids self.browse(to_install).button_install() return dict(ACTION_DICT, name=_('Apply Schedule Upgrade')) @assert_log_admin_access @api.multi def button_upgrade_cancel(self): self.write({'state': 'installed'}) return True @staticmethod def get_values_from_terp(terp): return { 'description': terp.get('description', ''), 'shortdesc': terp.get('name', ''), 'author': terp.get('author', 'Unknown'), 'maintainer': terp.get('maintainer', False), 'contributors': ', '.join(terp.get('contributors', [])) or False, 'website': terp.get('website', ''), 'license': terp.get('license', 'LGPL-3'), 'sequence': terp.get('sequence', 100), 'application': terp.get('application', False), 'auto_install': terp.get('auto_install', False), 'icon': terp.get('icon', False), 'summary': terp.get('summary', ''), 'url': terp.get('url') or terp.get('live_test_url', ''), } @api.model def create(self, vals): new = super(Module, self).create(vals) module_metadata = { 'name': 'module_%s' % vals['name'], 'model': 'ir.module.module', 'module': 'base', 'res_id': new.id, 'noupdate': True, } self.env['ir.model.data'].create(module_metadata) return new # update the list of available packages @assert_log_admin_access @api.model def update_list(self): res = [0, 0] # [update, add] default_version = modules.adapt_version('1.0') known_mods = self.search([]) known_mods_names = {mod.name: mod for mod in known_mods} # iterate through detected modules and update/create them in db for mod_name in modules.get_modules(): mod = known_mods_names.get(mod_name) terp = self.get_module_info(mod_name) values = self.get_values_from_terp(terp) if mod: updated_values = {} for key in values: old = getattr(mod, key) updated = tools.ustr(values[key]) if isinstance(values[key], pycompat.string_types) else values[key] if (old or updated) and updated != old: updated_values[key] = values[key] if terp.get('installable', True) and mod.state == 'uninstallable': updated_values['state'] = 'uninstalled' if parse_version(terp.get('version', default_version)) > parse_version(mod.latest_version or default_version): res[0] += 1 if updated_values: mod.write(updated_values) else: mod_path = modules.get_module_path(mod_name) if not mod_path: continue if not terp or not terp.get('installable', True): continue mod = self.create(dict(name=mod_name, state='uninstalled', **values)) res[1] += 1 mod._update_dependencies(terp.get('depends', [])) mod._update_exclusions(terp.get('excludes', [])) mod._update_category(terp.get('category', 'Uncategorized')) return res @assert_log_admin_access @api.multi def download(self, download=True): return [] @assert_log_admin_access @api.model def install_from_urls(self, urls): if not self.env.user.has_group('base.group_system'): raise AccessDenied() # One-click install is opt-in - cfr Issue #15225 ad_dir = tools.config.addons_data_dir if not os.access(ad_dir, os.W_OK): msg = (_("Automatic install of downloaded Apps is currently disabled.") + "\n\n" + _("To enable it, make sure this directory exists and is writable on the server:") + "\n%s" % ad_dir) _logger.warning(msg) raise UserError(msg) apps_server = urls.url_parse(self.get_apps_server()) OPENERP = actpy.release.product_name.lower() tmp = tempfile.mkdtemp() _logger.debug('Install from url: %r', urls) try: # 1. Download & unzip missing modules for module_name, url in urls.items(): if not url: continue # nothing to download, local version is already the last one up = urls.url_parse(url) if up.scheme != apps_server.scheme or up.netloc != apps_server.netloc: raise AccessDenied() try: _logger.info('Downloading module `%s` from actpy Apps', module_name) response = requests.get(url) response.raise_for_status() content = response.content except Exception: _logger.exception('Failed to fetch module %s', module_name) raise UserError(_('The `%s` module appears to be unavailable at the moment, please try again later.') % module_name) else: zipfile.ZipFile(io.BytesIO(content)).extractall(tmp) assert os.path.isdir(os.path.join(tmp, module_name)) # 2a. Copy/Replace module source in addons path for module_name, url in urls.items(): if module_name == OPENERP or not url: continue # OPENERP is special case, handled below, and no URL means local module module_path = modules.get_module_path(module_name, downloaded=True, display_warning=False) bck = backup(module_path, False) _logger.info('Copy downloaded module `%s` to `%s`', module_name, module_path) shutil.move(os.path.join(tmp, module_name), module_path) if bck: shutil.rmtree(bck) # 2b. Copy/Replace server+base module source if downloaded if urls.get(OPENERP): # special case. it contains the server and the base module. # extract path is not the same base_path = os.path.dirname(modules.get_module_path('base')) # copy all modules in the SERVER/actpy/addons directory to the new "actpy" module (except base itself) for d in os.listdir(base_path): if d != 'base' and os.path.isdir(os.path.join(base_path, d)): destdir = os.path.join(tmp, OPENERP, 'addons', d) # XXX 'actpy' subdirectory ? shutil.copytree(os.path.join(base_path, d), destdir) # then replace the server by the new "base" module server_dir = tools.config['root_path'] # XXX or dirname() bck = backup(server_dir) _logger.info('Copy downloaded module `actpy` to `%s`', server_dir) shutil.move(os.path.join(tmp, OPENERP), server_dir) #if bck: # shutil.rmtree(bck) self.update_list() with_urls = [module_name for module_name, url in urls.items() if url] downloaded = self.search([('name', 'in', with_urls)]) installed = self.search([('id', 'in', downloaded.ids), ('state', '=', 'installed')]) to_install = self.search([('name', 'in', list(urls)), ('state', '=', 'uninstalled')]) post_install_action = to_install.button_immediate_install() if installed or to_install: # in this case, force server restart to reload python code... self._cr.commit() actpy.service.server.restart() return { 'type': 'ir.actions.client', 'tag': 'home', 'params': {'wait': True}, } return post_install_action finally: shutil.rmtree(tmp) @api.model def get_apps_server(self): return tools.config.get('apps_server', 'https://apps.actpy.com/apps') def _update_dependencies(self, depends=None): existing = set(dep.name for dep in self.dependencies_id) needed = set(depends or []) for dep in (needed - existing): self._cr.execute('INSERT INTO ir_module_module_dependency (module_id, name) values (%s, %s)', (self.id, dep)) for dep in (existing - needed): self._cr.execute('DELETE FROM ir_module_module_dependency WHERE module_id = %s and name = %s', (self.id, dep)) self.invalidate_cache(['dependencies_id'], self.ids) def _update_exclusions(self, excludes=None): existing = set(excl.name for excl in self.exclusion_ids) needed = set(excludes or []) for name in (needed - existing): self._cr.execute('INSERT INTO ir_module_module_exclusion (module_id, name) VALUES (%s, %s)', (self.id, name)) for name in (existing - needed): self._cr.execute('DELETE FROM ir_module_module_exclusion WHERE module_id=%s AND name=%s', (self.id, name)) self.invalidate_cache(['exclusion_ids'], self.ids) def _update_category(self, category='Uncategorized'): current_category = self.category_id current_category_path = [] while current_category: current_category_path.insert(0, current_category.name) current_category = current_category.parent_id categs = category.split('/') if categs != current_category_path: cat_id = modules.db.create_categories(self._cr, categs) self.write({'category_id': cat_id}) @api.multi def _update_translations(self, filter_lang=None): if not filter_lang: langs = self.env['res.lang'].search([('translatable', '=', True)]) filter_lang = [lang.code for lang in langs] elif not isinstance(filter_lang, (list, tuple)): filter_lang = [filter_lang] update_mods = self.filtered(lambda r: r.state in ('installed', 'to install', 'to upgrade')) mod_dict = { mod.name: mod.dependencies_id.mapped('name') for mod in update_mods } mod_names = topological_sort(mod_dict) self.env['ir.translation'].load_module_terms(mod_names, filter_lang) @api.multi def _check(self): for module in self: if not module.description_html: _logger.warning('module %s: description is empty !', module.name) @api.model @tools.ormcache() def _installed(self): """ Return the set of installed modules as a dictionary {name: id} """ return { module.name: module.id for module in self.sudo().search([('state', '=', 'installed')]) }
class ResCompany(models.Model): _inherit = "res.company" #TODO check all the options/fields are in the views (settings + company form view) fiscalyear_last_day = fields.Integer(default=31, required=True) fiscalyear_last_month = fields.Selection([(1, 'January'), (2, 'February'), (3, 'March'), (4, 'April'), (5, 'May'), (6, 'June'), (7, 'July'), (8, 'August'), (9, 'September'), (10, 'October'), (11, 'November'), (12, 'December')], default=12, required=True) period_lock_date = fields.Date( string="Lock Date for Non-Advisers", help= "Only users with the 'Adviser' role can edit accounts prior to and inclusive of this date. Use it for period locking inside an open fiscal year, for example." ) fiscalyear_lock_date = fields.Date( string="Lock Date", help= "No users, including Advisers, can edit accounts prior to and inclusive of this date. Use it for fiscal year locking for example." ) transfer_account_id = fields.Many2one( 'account.account', domain=lambda self: [ ('reconcile', '=', True), ('user_type_id.id', '=', self.env.ref('account.data_account_type_current_assets').id), ('deprecated', '=', False) ], string="Inter-Banks Transfer Account", help= "Intermediary account used when moving money from a liquidity account to another" ) expects_chart_of_accounts = fields.Boolean( string='Expects a Chart of Accounts', default=True) chart_template_id = fields.Many2one( 'account.chart.template', help='The chart template for the company (if any)') bank_account_code_prefix = fields.Char( string='Prefix of the bank accounts', oldname="bank_account_code_char") cash_account_code_prefix = fields.Char( string='Prefix of the cash accounts') accounts_code_digits = fields.Integer( string='Number of digits in an account code') tax_cash_basis_journal_id = fields.Many2one('account.journal', string="Cash Basis Journal") tax_calculation_rounding_method = fields.Selection( [ ('round_per_line', 'Round per Line'), ('round_globally', 'Round Globally'), ], default='round_per_line', string='Tax Calculation Rounding Method') currency_exchange_journal_id = fields.Many2one( 'account.journal', string="Exchange Gain or Loss Journal", domain=[('type', '=', 'general')]) income_currency_exchange_account_id = fields.Many2one( 'account.account', related='currency_exchange_journal_id.default_credit_account_id', string="Gain Exchange Rate Account", domain= "[('internal_type', '=', 'other'), ('deprecated', '=', False), ('company_id', '=', id)]" ) expense_currency_exchange_account_id = fields.Many2one( 'account.account', related='currency_exchange_journal_id.default_debit_account_id', string="Loss Exchange Rate Account", domain= "[('internal_type', '=', 'other'), ('deprecated', '=', False), ('company_id', '=', id)]" ) anglo_saxon_accounting = fields.Boolean( string="Use anglo-saxon accounting") property_stock_account_input_categ_id = fields.Many2one( 'account.account', string="Input Account for Stock Valuation", oldname="property_stock_account_input_categ") property_stock_account_output_categ_id = fields.Many2one( 'account.account', string="Output Account for Stock Valuation", oldname="property_stock_account_output_categ") property_stock_valuation_account_id = fields.Many2one( 'account.account', string="Account Template for Stock Valuation") bank_journal_ids = fields.One2many('account.journal', 'company_id', domain=[('type', '=', 'bank')], string='Bank Journals') overdue_msg = fields.Text(string='Overdue Payments Message', translate=True, default=lambda s: _('''Dear Sir/Madam, Our records indicate that some payments on your account are still due. Please find details below. If the amount has already been paid, please disregard this notice. Otherwise, please forward us the total amount stated below. If you have any queries regarding your account, Please contact us. Thank you in advance for your cooperation. Best Regards,''')) tax_exigibility = fields.Boolean(string='Use Cash Basis') #Fields of the setup step for opening move account_opening_move_id = fields.Many2one( string='Opening Journal Entry', comodel_name='account.move', help= "The journal entry containing the initial balance of all this company's accounts." ) account_opening_journal_id = fields.Many2one( string='Opening Journal', comodel_name='account.journal', related='account_opening_move_id.journal_id', help= "Journal where the opening entry of this company's accounting has been posted." ) account_opening_date = fields.Date( string='Opening Date', related='account_opening_move_id.date', help= "Date at which the opening entry of this company's accounting has been posted." ) #Fields marking the completion of a setup step account_setup_company_data_done = fields.Boolean( string='Company Setup Marked As Done', help="Technical field holding the status of the company setup step.") account_setup_bank_data_done = fields.Boolean( 'Bank Setup Marked As Done', help="Technical field holding the status of the bank setup step.") account_setup_fy_data_done = fields.Boolean( 'Financial Year Setup Marked As Done', help= "Technical field holding the status of the financial year setup step.") account_setup_coa_done = fields.Boolean( string='Chart of Account Checked', help= "Technical field holding the status of the chart of account setup step." ) account_setup_bar_closed = fields.Boolean( string='Setup Bar Closed', help= "Technical field set to True when setup bar has been closed by the user." ) @api.model def _verify_fiscalyear_last_day(self, company_id, last_day, last_month): company = self.browse(company_id) last_day = last_day or (company and company.fiscalyear_last_day) or 31 last_month = last_month or (company and company.fiscalyear_last_month) or 12 current_year = datetime.now().year last_day_of_month = calendar.monthrange(current_year, last_month)[1] return last_day > last_day_of_month and last_day_of_month or last_day @api.multi def compute_fiscalyear_dates(self, date): """ Computes the start and end dates of the fiscalyear where the given 'date' belongs to @param date: a datetime object @returns: a dictionary with date_from and date_to """ self = self[0] last_month = self.fiscalyear_last_month last_day = self.fiscalyear_last_day if (date.month < last_month or (date.month == last_month and date.day <= last_day)): date = date.replace(month=last_month, day=last_day) else: if last_month == 2 and last_day == 29 and (date.year + 1) % 4 != 0: date = date.replace(month=last_month, day=28, year=date.year + 1) else: date = date.replace(month=last_month, day=last_day, year=date.year + 1) date_to = date date_from = date + timedelta(days=1) if date_from.month == 2 and date_from.day == 29: date_from = date_from.replace(day=28, year=date_from.year - 1) else: date_from = date_from.replace(year=date_from.year - 1) return {'date_from': date_from, 'date_to': date_to} def get_new_account_code(self, current_code, old_prefix, new_prefix, digits): return new_prefix + current_code.replace( old_prefix, '', 1).lstrip('0').rjust(digits - len(new_prefix), '0') def reflect_code_prefix_change(self, old_code, new_code, digits): accounts = self.env['account.account'].search( [('code', 'like', old_code), ('internal_type', '=', 'liquidity'), ('company_id', '=', self.id)], order='code asc') for account in accounts: if account.code.startswith(old_code): account.write({ 'code': self.get_new_account_code(account.code, old_code, new_code, digits) }) def reflect_code_digits_change(self, digits): accounts = self.env['account.account'].search( [('company_id', '=', self.id)], order='code asc') for account in accounts: account.write( {'code': account.code.rstrip('0').ljust(digits, '0')}) @api.multi def _validate_fiscalyear_lock(self, values): if values.get('fiscalyear_lock_date'): nb_draft_entries = self.env['account.move'].search([ ('company_id', 'in', [c.id for c in self]), ('state', '=', 'draft'), ('date', '<=', values['fiscalyear_lock_date']) ]) if nb_draft_entries: raise ValidationError( _('There are still unposted entries in the period you want to lock. You should either post or delete them.' )) @api.multi def write(self, values): #restrict the closing of FY if there are still unposted entries self._validate_fiscalyear_lock(values) # Reflect the change on accounts for company in self: digits = values.get( 'accounts_code_digits') or company.accounts_code_digits if values.get('bank_account_code_prefix') or values.get( 'accounts_code_digits'): new_bank_code = values.get( 'bank_account_code_prefix' ) or company.bank_account_code_prefix company.reflect_code_prefix_change( company.bank_account_code_prefix, new_bank_code, digits) if values.get('cash_account_code_prefix') or values.get( 'accounts_code_digits'): new_cash_code = values.get( 'cash_account_code_prefix' ) or company.cash_account_code_prefix company.reflect_code_prefix_change( company.cash_account_code_prefix, new_cash_code, digits) if values.get('accounts_code_digits'): company.reflect_code_digits_change(digits) #forbid the change of currency_id if there are already some accounting entries existing if 'currency_id' in values and values[ 'currency_id'] != company.currency_id.id: if self.env['account.move.line'].search([('company_id', '=', company.id)]): raise UserError( _('You cannot change the currency of the company since some journal items already exist' )) return super(ResCompany, self).write(values) @api.model def setting_init_company_action(self): """ Called by the 'Company Data' button of the setup bar.""" company = self.env.user.company_id view_id = self.env.ref('account.setup_view_company_form').id return { 'type': 'ir.actions.act_window', 'name': _('Company Data'), 'res_model': 'res.company', 'target': 'new', 'view_mode': 'form', 'res_id': company.id, 'views': [[view_id, 'form']], } @api.model def setting_init_bank_account_action(self): """ Called by the 'Bank Accounts' button of the setup bar.""" company = self.env.user.company_id view_id = self.env.ref('account.setup_bank_journal_form').id res = { 'type': 'ir.actions.act_window', 'name': _('Bank Account'), 'view_mode': 'form', 'res_model': 'account.journal', 'target': 'new', 'views': [[view_id, 'form']], } # If some bank journal already exists, we open it in the form, so the user can edit it. # Otherwise, we just open the form in creation mode. bank_journal = self.env['account.journal'].search( [('company_id', '=', company.id), ('type', '=', 'bank')], limit=1) if bank_journal: res['res_id'] = bank_journal.id else: res['context'] = {'default_type': 'bank'} return res @api.model def setting_init_fiscal_year_action(self): """ Called by the 'Fiscal Year Opening' button of the setup bar.""" company = self.env.user.company_id company.create_op_move_if_non_existant() new_wizard = self.env['account.financial.year.op'].create( {'company_id': company.id}) view_id = self.env.ref('account.setup_financial_year_opening_form').id return { 'type': 'ir.actions.act_window', 'name': _('Fiscal Year'), 'view_mode': 'form', 'res_model': 'account.financial.year.op', 'target': 'new', 'res_id': new_wizard.id, 'views': [[view_id, 'form']], } @api.model def setting_chart_of_accounts_action(self): """ Called by the 'Chart of Accounts' button of the setup bar.""" company = self.env.user.company_id company.account_setup_coa_done = True # If an opening move has already been posted, we open the tree view showing all the accounts if company.opening_move_posted(): return 'account.action_account_form' # Otherwise, we create the opening move company.create_op_move_if_non_existant() # Then, we open will open a custom tree view allowing to edit opening balances of the account view_id = self.env.ref('account.init_accounts_tree').id # Hide the current year earnings account as it is automatically computed domain = [('user_type_id', '!=', self.env.ref('account.data_unaffected_earnings').id), ('company_id', '=', company.id)] return { 'type': 'ir.actions.act_window', 'name': _('Chart of Accounts'), 'res_model': 'account.account', 'view_mode': 'tree', 'search_view_id': self.env.ref('account.view_account_search').id, 'views': [[view_id, 'list']], 'domain': domain, } @api.model def setting_opening_move_action(self): """ Called by the 'Initial Balances' button of the setup bar.""" company = self.env.user.company_id # If the opening move has already been posted, we open its form view if company.opening_move_posted(): form_view_id = self.env.ref('account.setup_posted_move_form').id return { 'type': 'ir.actions.act_window', 'name': _('Initial Balances'), 'view_mode': 'form', 'res_model': 'account.move', 'target': 'new', 'res_id': company.account_opening_move_id.id, 'views': [[form_view_id, 'form']], } # Otherwise, we open a custom wizard to post it. company.create_op_move_if_non_existant() new_wizard = self.env['account.opening'].create( {'company_id': company.id}) view_id = self.env.ref('account.setup_opening_move_wizard_form').id return { 'type': 'ir.actions.act_window', 'name': _('Initial Balances'), 'view_mode': 'form', 'res_model': 'account.opening', 'target': 'new', 'res_id': new_wizard.id, 'views': [[view_id, 'form']], 'context': { 'check_move_validity': False }, } @api.model def setting_hide_setup_bar(self): """ Called by the cross button of the setup bar, to close it.""" self.env.user.company_id.account_setup_bar_closed = True @api.model def create_op_move_if_non_existant(self): """ Creates an empty opening move in 'draft' state for the current company if there wasn't already one defined. For this, the function needs at least one journal of type 'general' to exist (required by account.move). """ self.ensure_one() if not self.account_opening_move_id: default_journal = self.env['account.journal'].search( [('type', '=', 'general'), ('company_id', '=', self.id)], limit=1) if not default_journal: raise UserError( _("Please install a chart of accounts or create a miscellaneous journal before proceeding." )) self.account_opening_move_id = self.env['account.move'].create({ 'name': _('Opening Journal Entry'), 'company_id': self.id, 'journal_id': default_journal.id, }) def mark_company_setup_as_done_action(self): """ Marks the 'company' setup step as completed.""" self.account_setup_company_data_done = True def unmark_company_setup_as_done_action(self): """ Marks the 'company' setup step as uncompleted.""" self.account_setup_company_data_done = False def opening_move_posted(self): """ Returns true if this company has an opening account move and this move is posted.""" return bool(self.account_opening_move_id ) and self.account_opening_move_id.state == 'posted' def get_unaffected_earnings_account(self): """ Returns the unaffected earnings account for this company, creating one if none has yet been defined. """ unaffected_earnings_type = self.env.ref( "account.data_unaffected_earnings") account = self.env['account.account'].search([ ('company_id', '=', self.id), ('user_type_id', '=', unaffected_earnings_type.id) ]) if account: return account[0] return self.env['account.account'].create({ 'code': '999999', 'name': _('Undistributed Profits/Losses'), 'user_type_id': unaffected_earnings_type.id, 'company_id': self.id, }) def get_opening_move_differences(self, opening_move_lines): currency = self.currency_id balancing_move_line = opening_move_lines.filtered( lambda x: x.account_id == self.get_unaffected_earnings_account()) debits_sum = credits_sum = 0.0 for line in opening_move_lines: if line != balancing_move_line: #skip the autobalancing move line debits_sum += line.debit credits_sum += line.credit difference = abs(debits_sum - credits_sum) debit_diff = (debits_sum > credits_sum) and float_round( difference, precision_rounding=currency.rounding) or 0.0 credit_diff = (debits_sum < credits_sum) and float_round( difference, precision_rounding=currency.rounding) or 0.0 return debit_diff, credit_diff def _auto_balance_opening_move(self): """ Checks the opening_move of this company. If it has not been posted yet and is unbalanced, balances it with a automatic account.move.line in the current year earnings account. """ if self.account_opening_move_id and self.account_opening_move_id.state == 'draft': debit_diff, credit_diff = self.get_opening_move_differences( self.account_opening_move_id.line_ids) currency = self.currency_id balancing_move_line = self.account_opening_move_id.line_ids.filtered( lambda x: x.account_id == self.get_unaffected_earnings_account( )) if float_is_zero(debit_diff + credit_diff, precision_rounding=currency.rounding): if balancing_move_line: # zero difference and existing line : delete the line balancing_move_line.unlink() else: if balancing_move_line: # Non-zero difference and existing line : edit the line balancing_move_line.write({ 'debit': credit_diff, 'credit': debit_diff }) else: # Non-zero difference and no existing line : create a new line balancing_account = self.get_unaffected_earnings_account() self.env['account.move.line'].create({ 'name': _('Automatic Balancing Line'), 'move_id': self.account_opening_move_id.id, 'account_id': balancing_account.id, 'debit': credit_diff, 'credit': debit_diff, })
class GamificationBadge(models.Model): """Badge object that users can send and receive""" CAN_GRANT = 1 NOBODY_CAN_GRANT = 2 USER_NOT_VIP = 3 BADGE_REQUIRED = 4 TOO_MANY = 5 _name = 'gamification.badge' _description = 'Gamification badge' _inherit = ['mail.thread'] name = fields.Char('Badge', required=True, translate=True) active = fields.Boolean('Active', default=True) description = fields.Text('Description', translate=True) image = fields.Binary( "Image", attachment=True, help="This field holds the image used for the badge, limited to 256x256" ) rule_auth = fields.Selection([ ('everyone', 'Everyone'), ('users', 'A selected list of users'), ('having', 'People having some badges'), ('nobody', 'No one, assigned through challenges'), ], default='everyone', string="Allowance to Grant", help="Who can grant this badge", required=True) rule_auth_user_ids = fields.Many2many( 'res.users', 'rel_badge_auth_users', string='Authorized Users', help="Only these people can give this badge") rule_auth_badge_ids = fields.Many2many( 'gamification.badge', 'gamification_badge_rule_badge_rel', 'badge1_id', 'badge2_id', string='Required Badges', help="Only the people having these badges can give this badge") rule_max = fields.Boolean( 'Monthly Limited Sending', help="Check to set a monthly limit per person of sending this badge") rule_max_number = fields.Integer( 'Limitation Number', help= "The maximum number of time this badge can be sent per month per person." ) challenge_ids = fields.One2many('gamification.challenge', 'reward_id', string="Reward of Challenges") goal_definition_ids = fields.Many2many( 'gamification.goal.definition', 'badge_unlocked_definition_rel', string='Rewarded by', help= "The users that have succeeded theses goals will receive automatically the badge." ) owner_ids = fields.One2many( 'gamification.badge.user', 'badge_id', string='Owners', help='The list of instances of this badge granted to users') stat_count = fields.Integer( "Total", compute='_get_owners_info', help="The number of time this badge has been received.") stat_count_distinct = fields.Integer( "Number of users", compute='_get_owners_info', help="The number of time this badge has been received by unique users." ) unique_owner_ids = fields.Many2many( 'res.users', string="Unique Owners", compute='_get_owners_info', help="The list of unique users having received this badge.") stat_this_month = fields.Integer( "Monthly total", compute='_get_badge_user_stats', help="The number of time this badge has been received this month.") stat_my = fields.Integer( "My Total", compute='_get_badge_user_stats', help="The number of time the current user has received this badge.") stat_my_this_month = fields.Integer( "My Monthly Total", compute='_get_badge_user_stats', help= "The number of time the current user has received this badge this month." ) stat_my_monthly_sending = fields.Integer( 'My Monthly Sending Total', compute='_get_badge_user_stats', help= "The number of time the current user has sent this badge this month.") remaining_sending = fields.Integer("Remaining Sending Allowed", compute='_remaining_sending_calc', help="If a maximum is set") @api.depends('owner_ids') def _get_owners_info(self): """Return: the list of unique res.users ids having received this badge the total number of time this badge was granted the total number of users this badge was granted to """ self.env.cr.execute( """ SELECT badge_id, count(user_id) as stat_count, count(distinct(user_id)) as stat_count_distinct, array_agg(distinct(user_id)) as unique_owner_ids FROM gamification_badge_user WHERE badge_id in %s GROUP BY badge_id """, [tuple(self.ids)]) defaults = { 'stat_count': 0, 'stat_count_distinct': 0, 'unique_owner_ids': [], } mapping = { badge_id: { 'stat_count': count, 'stat_count_distinct': distinct_count, 'unique_owner_ids': owner_ids, } for (badge_id, count, distinct_count, owner_ids) in self.env.cr._obj } for badge in self: badge.update(mapping.get(badge.id, defaults)) @api.depends('owner_ids.badge_id', 'owner_ids.create_date', 'owner_ids.user_id') def _get_badge_user_stats(self): """Return stats related to badge users""" first_month_day = fields.Date.to_string(date.today().replace(day=1)) for badge in self: owners = badge.owner_ids badge.stats_my = sum(o.user_id == self.env.user for o in owners) badge.stats_this_month = sum(o.create_date >= first_month_day for o in owners) badge.stats_my_this_month = sum( o.user_id == self.env.user and o.create_date >= first_month_day for o in owners) badge.stats_my_monthly_sending = sum( o.create_uid == self.env.user and o.create_date >= first_month_day for o in owners) @api.depends( 'rule_auth', 'rule_auth_user_ids', 'rule_auth_badge_ids', 'rule_max', 'rule_max_number', 'stat_my_monthly_sending', ) def _remaining_sending_calc(self): """Computes the number of badges remaining the user can send 0 if not allowed or no remaining integer if limited sending -1 if infinite (should not be displayed) """ for badge in self: if badge._can_grant_badge() != self.CAN_GRANT: # if the user cannot grant this badge at all, result is 0 badge.remaining_sending = 0 elif not badge.rule_max: # if there is no limitation, -1 is returned which means 'infinite' badge.remaining_sending = -1 else: badge.remaining_sending = badge.rule_max_number - badge.stat_my_monthly_sending def check_granting(self): """Check the user 'uid' can grant the badge 'badge_id' and raise the appropriate exception if not Do not check for SUPERUSER_ID """ status_code = self._can_grant_badge() if status_code == self.CAN_GRANT: return True elif status_code == self.NOBODY_CAN_GRANT: raise exceptions.UserError( _('This badge can not be sent by users.')) elif status_code == self.USER_NOT_VIP: raise exceptions.UserError( _('You are not in the user allowed list.')) elif status_code == self.BADGE_REQUIRED: raise exceptions.UserError( _('You do not have the required badges.')) elif status_code == self.TOO_MANY: raise exceptions.UserError( _('You have already sent this badge too many time this month.') ) else: _logger.error("Unknown badge status code: %s" % status_code) return False def _can_grant_badge(self): """Check if a user can grant a badge to another user :param uid: the id of the res.users trying to send the badge :param badge_id: the granted badge id :return: integer representing the permission. """ if self.env.user._is_admin(): return self.CAN_GRANT if self.rule_auth == 'nobody': return self.NOBODY_CAN_GRANT elif self.rule_auth == 'users' and self.env.user not in self.rule_auth_user_ids: return self.USER_NOT_VIP elif self.rule_auth == 'having': all_user_badges = self.env['gamification.badge.user'].search([ ('user_id', '=', self.env.uid) ]) if self.rule_auth_badge_ids - all_user_badges: return self.BADGE_REQUIRED if self.rule_max and self.stat_my_monthly_sending >= self.rule_max_number: return self.TOO_MANY # badge.rule_auth == 'everyone' -> no check return self.CAN_GRANT
class Users(models.Model): _inherit = "res.users" bookmark_ids = fields.One2many('menu.bookmark', 'user_id', string="Bookmark Records")
class CrmTeam(models.Model): _name = "crm.team" _inherit = ['mail.thread', 'ir.branch.company.mixin'] _description = "Sales Channel" _order = "name" @api.model @api.returns('self', lambda value: value.id if value else False) def _get_default_team_id(self, user_id=None): if not user_id: user_id = self.env.uid company_id = self.sudo(user_id).env.user.company_id.id team_id = self.env['crm.team'].sudo().search([ '|', ('user_id', '=', user_id), ('member_ids', '=', user_id), '|', ('company_id', '=', False), ('company_id', 'child_of', [company_id]) ], limit=1) if not team_id and 'default_team_id' in self.env.context: team_id = self.env['crm.team'].browse(self.env.context.get('default_team_id')) if not team_id: default_team_id = self.env.ref('sales_team.team_sales_department', raise_if_not_found=False) if default_team_id and (self.env.context.get('default_type') != 'lead' or default_team_id.use_leads): team_id = default_team_id return team_id def _get_default_favorite_user_ids(self): return [(6, 0, [self.env.uid])] name = fields.Char('Sales Channel', required=True, translate=True) active = fields.Boolean(default=True, help="If the active field is set to false, it will allow you to hide the sales channel without removing it.") company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env['res.company']._company_default_get('crm.team')) currency_id = fields.Many2one( "res.currency", related='company_id.currency_id', string="Currency", readonly=True) user_id = fields.Many2one('res.users', string='Channel Leader') member_ids = fields.One2many('res.users', 'sale_team_id', string='Channel Members') favorite_user_ids = fields.Many2many( 'res.users', 'team_favorite_user_rel', 'team_id', 'user_id', string='Favorite Members', default=_get_default_favorite_user_ids) is_favorite = fields.Boolean( string='Show on dashboard', compute='_compute_is_favorite', inverse='_inverse_is_favorite', help="Favorite teams to display them in the dashboard and access them easily.") reply_to = fields.Char(string='Reply-To', help="The email address put in the 'Reply-To' of all emails sent by actpy about cases in this sales channel") color = fields.Integer(string='Color Index', help="The color of the channel") team_type = fields.Selection([('sales', 'Sales'), ('website', 'Website')], string='Channel Type', default='sales', required=True, help="The type of this channel, it will define the resources this channel uses.") dashboard_button_name = fields.Char(string="Dashboard Button", compute='_compute_dashboard_button_name') dashboard_graph_data = fields.Text(compute='_compute_dashboard_graph') dashboard_graph_type = fields.Selection([ ('line', 'Line'), ('bar', 'Bar'), ], string='Type', compute='_compute_dashboard_graph', help='The type of graph this channel will display in the dashboard.') dashboard_graph_model = fields.Selection([], string="Content", help='The graph this channel will display in the Dashboard.\n') dashboard_graph_group = fields.Selection([ ('day', 'Day'), ('week', 'Week'), ('month', 'Month'), ('user', 'Salesperson'), ], string='Group by', default='day', help="How this channel's dashboard graph will group the results.") dashboard_graph_period = fields.Selection([ ('week', 'Last Week'), ('month', 'Last Month'), ('year', 'Last Year'), ], string='Scale', default='month', help="The time period this channel's dashboard graph will consider.") @api.constrains('company_id', 'branch_id') def _check_company_branch(self): for record in self: if record.branch_id and record.company_id and record.company_id != record.branch_id.company_id: raise ValidationError( _('Configuration Error of Company:\n' 'The Company (%s) in the Team and ' 'the Company (%s) of Branch must ' 'be the same company!') % (record.company_id.name, record.branch_id.company_id.name) ) @api.depends('dashboard_graph_group', 'dashboard_graph_model', 'dashboard_graph_period') def _compute_dashboard_graph(self): for team in self.filtered('dashboard_graph_model'): if team.dashboard_graph_group in (False, 'user') or team.dashboard_graph_period == 'week' and team.dashboard_graph_group != 'day' \ or team.dashboard_graph_period == 'month' and team.dashboard_graph_group != 'day': team.dashboard_graph_type = 'bar' else: team.dashboard_graph_type = 'line' team.dashboard_graph_data = json.dumps(team._get_graph()) def _compute_is_favorite(self): for team in self: team.is_favorite = self.env.user in team.favorite_user_ids def _inverse_is_favorite(self): sudoed_self = self.sudo() to_fav = sudoed_self.filtered(lambda team: self.env.user not in team.favorite_user_ids) to_fav.write({'favorite_user_ids': [(4, self.env.uid)]}) (sudoed_self - to_fav).write({'favorite_user_ids': [(3, self.env.uid)]}) return True def _graph_get_dates(self, today): """ return a coherent start and end date for the dashboard graph according to the graph settings. """ if self.dashboard_graph_period == 'week': start_date = today - relativedelta(weeks=1) elif self.dashboard_graph_period == 'year': start_date = today - relativedelta(years=1) else: start_date = today - relativedelta(months=1) # we take the start of the following month/week/day if we group by month/week/day # (to avoid having twice the same month/week/day from different years/month/week) if self.dashboard_graph_group == 'month': start_date = date(start_date.year + start_date.month // 12, start_date.month % 12 + 1, 1) # handle period=week, grouping=month for silly managers if self.dashboard_graph_period == 'week': start_date = today.replace(day=1) elif self.dashboard_graph_group == 'week': start_date += relativedelta(days=8 - start_date.isocalendar()[2]) # add a week to make sure no overlapping is possible in case of year period (will display max 52 weeks, avoid case of 53 weeks in a year) if self.dashboard_graph_period == 'year': start_date += relativedelta(weeks=1) else: start_date += relativedelta(days=1) return [start_date, today] def _graph_date_column(self): return 'create_date' def _graph_x_query(self): if self.dashboard_graph_group == 'user': return 'user_id' elif self.dashboard_graph_group == 'week': return 'EXTRACT(WEEK FROM %s)' % self._graph_date_column() elif self.dashboard_graph_group == 'month': return 'EXTRACT(MONTH FROM %s)' % self._graph_date_column() else: return 'DATE(%s)' % self._graph_date_column() def _graph_y_query(self): raise UserError(_('Undefined graph model for Sales Channel: %s') % self.name) def _extra_sql_conditions(self): return '' def _graph_title_and_key(self): """ Returns an array containing the appropriate graph title and key respectively. The key is for lineCharts, to have the on-hover label. """ return ['', ''] def _graph_data(self, start_date, end_date): """ return format should be an iterable of dicts that contain {'x_value': ..., 'y_value': ...} x_values should either be dates, weeks, months or user_ids depending on the self.dashboard_graph_group value. y_values are floats. """ query = """SELECT %(x_query)s as x_value, %(y_query)s as y_value FROM %(table)s WHERE team_id = %(team_id)s AND DATE(%(date_column)s) >= %(start_date)s AND DATE(%(date_column)s) <= %(end_date)s %(extra_conditions)s GROUP BY x_value;""" # apply rules if not self.dashboard_graph_model: raise UserError(_('Undefined graph model for Sales Channel: %s') % self.name) GraphModel = self.env[self.dashboard_graph_model] graph_table = GraphModel._table extra_conditions = self._extra_sql_conditions() where_query = GraphModel._where_calc([]) GraphModel._apply_ir_rules(where_query, 'read') from_clause, where_clause, where_clause_params = where_query.get_sql() if where_clause: extra_conditions += " AND " + where_clause query = query % { 'x_query': self._graph_x_query(), 'y_query': self._graph_y_query(), 'table': graph_table, 'team_id': "%s", 'date_column': self._graph_date_column(), 'start_date': "%s", 'end_date': "%s", 'extra_conditions': extra_conditions } self._cr.execute(query, [self.id, start_date, end_date] + where_clause_params) return self.env.cr.dictfetchall() def _get_graph(self): def get_week_name(start_date, locale): """ Generates a week name (string) from a datetime according to the locale: E.g.: locale start_date (datetime) return string "en_US" November 16th "16-22 Nov" "en_US" December 28th "28 Dec-3 Jan" """ if (start_date + relativedelta(days=6)).month == start_date.month: short_name_from = format_date(start_date, 'd', locale=locale) else: short_name_from = format_date(start_date, 'd MMM', locale=locale) short_name_to = format_date(start_date + relativedelta(days=6), 'd MMM', locale=locale) return short_name_from + '-' + short_name_to self.ensure_one() values = [] today = date.today() start_date, end_date = self._graph_get_dates(today) graph_data = self._graph_data(start_date, end_date) # line graphs and bar graphs require different labels if self.dashboard_graph_type == 'line': x_field = 'x' y_field = 'y' else: x_field = 'label' y_field = 'value' # generate all required x_fields and update the y_values where we have data for them locale = self._context.get('lang') or 'en_US' if self.dashboard_graph_group == 'day': for day in range(0, (end_date - start_date).days + 1): short_name = format_date(start_date + relativedelta(days=day), 'd MMM', locale=locale) values.append({x_field: short_name, y_field: 0}) for data_item in graph_data: index = (datetime.strptime(data_item.get('x_value'), DF).date() - start_date).days values[index][y_field] = data_item.get('y_value') elif self.dashboard_graph_group == 'week': weeks_in_start_year = int(date(start_date.year, 12, 28).isocalendar()[1]) # This date is always in the last week of ISO years for week in range(0, (end_date.isocalendar()[1] - start_date.isocalendar()[1]) % weeks_in_start_year + 1): short_name = get_week_name(start_date + relativedelta(days=7 * week), locale) values.append({x_field: short_name, y_field: 0}) for data_item in graph_data: index = int((data_item.get('x_value') - start_date.isocalendar()[1]) % weeks_in_start_year) values[index][y_field] = data_item.get('y_value') elif self.dashboard_graph_group == 'month': for month in range(0, (end_date.month - start_date.month) % 12 + 1): short_name = format_date(start_date + relativedelta(months=month), 'MMM', locale=locale) values.append({x_field: short_name, y_field: 0}) for data_item in graph_data: index = int((data_item.get('x_value') - start_date.month) % 12) values[index][y_field] = data_item.get('y_value') elif self.dashboard_graph_group == 'user': for data_item in graph_data: values.append({x_field: self.env['res.users'].browse(data_item.get('x_value')).name or _('Not Defined'), y_field: data_item.get('y_value')}) else: for data_item in graph_data: values.append({x_field: data_item.get('x_value'), y_field: data_item.get('y_value')}) [graph_title, graph_key] = self._graph_title_and_key() color = '#875A7B' if '+e' in version else '#7c7bad' return [{'values': values, 'area': True, 'title': graph_title, 'key': graph_key, 'color': color}] def _compute_dashboard_button_name(self): """ Sets the adequate dashboard button name depending on the sales channel's options """ for team in self: team.dashboard_button_name = _("Big Pretty Button :)") # placeholder def action_primary_channel_button(self): """ skeleton function to be overloaded It will return the adequate action depending on the sales channel's options """ return False def _onchange_team_type(self): """ skeleton function defined here because it'll be called by crm and/or sale """ self.ensure_one() @api.model def create(self, values): team = super(CrmTeam, self.with_context(mail_create_nosubscribe=True)).create(values) if values.get('member_ids'): team._add_members_to_favorites() return team @api.multi def write(self, values): res = super(CrmTeam, self).write(values) if values.get('member_ids'): self._add_members_to_favorites() return res def _add_members_to_favorites(self): for team in self: team.favorite_user_ids = [(4, member.id) for member in team.member_ids]
class IrActionsServer(models.Model): """ Server actions model. Server action work on a base model and offer various type of actions that can be executed automatically, for example using base action rules, of manually, by adding the action in the 'More' contextual menu. Since actpy 8.0 a button 'Create Menu Action' button is available on the action form view. It creates an entry in the More menu of the base model. This allows to create server actions and run them in mass mode easily through the interface. The available actions are : - 'Execute Python Code': a block of python code that will be executed - 'Run a Client Action': choose a client action to launch - 'Create or Copy a new Record': create a new record with new values, or copy an existing record in your database - 'Write on a Record': update the values of a record - 'Execute several actions': define an action that triggers several other server actions """ _name = 'ir.actions.server' _table = 'ir_act_server' _inherit = 'ir.actions.actions' _sequence = 'ir_actions_id_seq' _order = 'sequence,name' DEFAULT_PYTHON_CODE = """# Available variables: # - env: actpy Environment on which the action is triggered # - model: actpy Model of the record on which the action is triggered; is a void recordset # - record: record on which the action is triggered; may be be void # - records: recordset of all records on which the action is triggered in multi-mode; may be void # - time, datetime, dateutil, timezone: useful Python libraries # - log: log(message, level='info'): logging function to record debug information in ir.logging table # - Warning: Warning Exception to use with raise # To return an action, assign: action = {...}\n\n\n\n""" @api.model def _select_objects(self): records = self.env['ir.model'].search([]) return [(record.model, record.name) for record in records] + [('', '')] name = fields.Char(string='Action Name', translate=True) type = fields.Char(default='ir.actions.server') usage = fields.Selection([('ir_actions_server', 'Server Action'), ('ir_cron', 'Scheduled Action')], string='Usage', default='ir_actions_server', required=True) state = fields.Selection( [('code', 'Execute Python Code'), ('object_create', 'Create a new Record'), ('object_write', 'Update the Record'), ('multi', 'Execute several actions')], string='Action To Do', default='object_write', required=True, help="Type of server action. The following values are available:\n" "- 'Execute Python Code': a block of python code that will be executed\n" "- 'Create or Copy a new Record': create a new record with new values, or copy an existing record in your database\n" "- 'Write on a Record': update the values of a record\n" "- 'Execute several actions': define an action that triggers several other server actions\n" "- 'Add Followers': add followers to a record (available in Discuss)\n" "- 'Send Email': automatically send an email (available in email_template)" ) # Generic sequence = fields.Integer( default=5, help="When dealing with multiple actions, the execution order is " "based on the sequence. Low number means high priority.") model_id = fields.Many2one('ir.model', string='Model', required=True, ondelete='cascade', help="Model on which the server action runs.") model_name = fields.Char(related='model_id.model', readonly=True, store=True) # Python code code = fields.Text( string='Python Code', groups='base.group_system', default=DEFAULT_PYTHON_CODE, help= "Write Python code that the action will execute. Some variables are " "available for use; help about pyhon expression is given in the help tab." ) # Multi child_ids = fields.Many2many( 'ir.actions.server', 'rel_server_actions', 'server_id', 'action_id', string='Child Actions', help= 'Child server actions that will be executed. Note that the last return returned action value will be used as global return value.' ) # Create crud_model_id = fields.Many2one( 'ir.model', string='Create/Write Target Model', oldname='srcmodel_id', help= "Model for record creation / update. Set this field only to specify a different model than the base model." ) crud_model_name = fields.Char(related='crud_model_id.name', readonly=True) link_field_id = fields.Many2one( 'ir.model.fields', string='Link using field', help="Provide the field used to link the newly created record " "on the record on used by the server action.") fields_lines = fields.One2many('ir.server.object.lines', 'server_id', string='Value Mapping', copy=True) @api.constrains('code') def _check_python_code(self): for action in self.sudo().filtered('code'): msg = test_python_expr(expr=action.code.strip(), mode="exec") if msg: raise ValidationError(msg) @api.constrains('child_ids') def _check_recursion(self): if not self._check_m2m_recursion('child_ids'): raise ValidationError(_('Recursion found in child server actions')) @api.onchange('crud_model_id') def _onchange_crud_model_id(self): self.link_field_id = False self.crud_model_name = self.crud_model_id.model @api.onchange('model_id') def _onchange_model_id(self): self.model_name = self.model_id.model @api.multi def create_action(self): """ Create a contextual action for each server action. """ for action in self: action.write({ 'binding_model_id': action.model_id.id, 'binding_type': 'action' }) return True @api.multi def unlink_action(self): """ Remove the contextual actions created for the server actions. """ self.check_access_rights('write', raise_exception=True) self.filtered('binding_model_id').write({'binding_model_id': False}) return True @api.model def run_action_code_multi(self, action, eval_context=None): safe_eval(action.sudo().code.strip(), eval_context, mode="exec", nocopy=True) # nocopy allows to return 'action' if 'action' in eval_context: return eval_context['action'] @api.model def run_action_multi(self, action, eval_context=None): res = False for act in action.child_ids: result = act.run() if result: res = result return res @api.model def run_action_object_write(self, action, eval_context=None): """ Write server action. - 1. evaluate the value mapping - 2. depending on the write configuration: - `current`: id = active_id - `other`: id = from reference object - `expression`: id = from expression evaluation """ res = {} for exp in action.fields_lines: res[exp.col1.name] = exp.eval_value( eval_context=eval_context)[exp.id] if self._context.get('onchange_self'): record_cached = self._context['onchange_self'] for field, new_value in res.items(): record_cached[field] = new_value else: self.env[action.model_id.model].browse( self._context.get('active_id')).write(res) @api.model def run_action_object_create(self, action, eval_context=None): """ Create and Copy server action. - 1. evaluate the value mapping - 2. depending on the write configuration: - `new`: new record in the base model - `copy_current`: copy the current record (id = active_id) + gives custom values - `new_other`: new record in target model - `copy_other`: copy the current record (id from reference object) + gives custom values """ res = {} for exp in action.fields_lines: res[exp.col1.name] = exp.eval_value( eval_context=eval_context)[exp.id] res = self.env[action.crud_model_id.model].create(res) if action.link_field_id: record = self.env[action.model_id.model].browse( self._context.get('active_id')) record.write({action.link_field_id.name: res.id}) @api.model def _get_eval_context(self, action=None): """ Prepare the context used when evaluating python code, like the python formulas or code server actions. :param action: the current server action :type action: browse record :returns: dict -- evaluation context given to (safe_)safe_eval """ def log(message, level="info"): with self.pool.cursor() as cr: cr.execute( """ INSERT INTO ir_logging(create_date, create_uid, type, dbname, name, level, message, path, line, func) VALUES (NOW() at time zone 'UTC', %s, %s, %s, %s, %s, %s, %s, %s, %s) """, (self.env.uid, 'server', self._cr.dbname, __name__, level, message, "action", action.id, action.name)) eval_context = super(IrActionsServer, self)._get_eval_context(action=action) model_name = action.model_id.sudo().model model = self.env[model_name] record = None records = None if self._context.get( 'active_model') == model_name and self._context.get( 'active_id'): record = model.browse(self._context['active_id']) if self._context.get( 'active_model') == model_name and self._context.get( 'active_ids'): records = model.browse(self._context['active_ids']) if self._context.get('onchange_self'): record = self._context['onchange_self'] eval_context.update({ # orm 'env': self.env, 'model': model, # Exceptions 'Warning': actpy.exceptions.Warning, # record 'record': record, 'records': records, # helpers 'log': log, }) return eval_context @api.multi def run(self): """ Runs the server action. For each server action, the run_action_<STATE> method is called. This allows easy overriding of the server actions. :param dict context: context should contain following keys - active_id: id of the current object (single mode) - active_model: current model that should equal the action's model The following keys are optional: - active_ids: ids of the current records (mass mode). If active_ids and active_id are present, active_ids is given precedence. :return: an action_id to be executed, or False is finished correctly without return action """ res = False for action in self: eval_context = self._get_eval_context(action) if hasattr(self, 'run_action_%s_multi' % action.state): # call the multi method run_self = self.with_context(eval_context['env'].context) func = getattr(run_self, 'run_action_%s_multi' % action.state) res = func(action, eval_context=eval_context) elif hasattr(self, 'run_action_%s' % action.state): active_id = self._context.get('active_id') if not active_id and self._context.get('onchange_self'): active_id = self._context['onchange_self']._origin.id active_ids = self._context.get( 'active_ids', [active_id] if active_id else []) for active_id in active_ids: # run context dedicated to a particular active_id run_self = self.with_context(active_ids=[active_id], active_id=active_id) eval_context["env"].context = run_self._context # call the single method related to the action: run_action_<STATE> func = getattr(run_self, 'run_action_%s' % action.state) res = func(action, eval_context=eval_context) return res @api.model def _run_actions(self, ids): """ Run server actions with given ids. Allow crons to run specific server actions """ return self.browse(ids).run()
class PriceRules(models.Model): _name = 'price.rule' _description = 'Price Rules' _order = 'sequence' @api.multi @api.depends('apply_on', 'categ_id', 'product_tmpl_id', 'product_id') def _get_pricerule_name_price(self): for record in self: if record.categ_id: record.name = _("Category: %s") % (record.categ_id.name) elif record.product_tmpl_id: record.name = record.product_tmpl_id.name elif record.product_id: record.name = record.product_id.display_name.replace( '[%s]' % record.product_id.code, '') else: record.name = _("All Products") name = fields.Char('Name', compute='_get_pricerule_name_price') sequence = fields.Integer('Sequence') apply_on = fields.Selection([('all', 'Global'), ('category', 'Category'), ('product_template', 'Product Template'), ('product', 'Product Variant')], required=True, default='all', string="Apply On") categ_id = fields.Many2one('product.category', 'Category') product_tmpl_id = fields.Many2one('product.template', 'Product Template') product_id = fields.Many2one('product.product', 'Product') active = fields.Boolean('Active', default=True) start_date = fields.Date('Start Date') end_date = fields.Date('End Date') note = fields.Text('Description') pricelist_id = fields.Many2one('product.pricelist', 'Pricelist', index=True, ondelete='cascade') rule_lines = fields.One2many('rule.line', 'price_rule_id', 'Product Rule Lines') @api.onchange('apply_on') def _onchange_apply_on(self): if self.apply_on != 'product': self.product_id = False if self.apply_on != 'product_template': self.product_tmpl_id = False if self.apply_on != 'category': self.categ_id = False @api.multi def get_rules(self, pricelist_id, date): date = fields.Date.context_today(self) self._cr.execute( 'SELECT rule.id ' 'FROM price_rule AS rule ' 'WHERE (rule.pricelist_id = %s) ' 'AND (rule.start_date IS NULL OR rule.start_date<=%s) ' 'AND (rule.end_date IS NULL OR rule.end_date>=%s)' 'ORDER BY rule.sequence', (pricelist_id.id, date, date)) rules_ids = [x[0] for x in self._cr.fetchall()] rules = self.browse(rules_ids) return rules
class QuantPackage(models.Model): """ Packages containing quants and/or other packages """ _name = "stock.quant.package" _description = "Physical Packages" _order = 'name' name = fields.Char( 'Package Reference', copy=False, index=True, default=lambda self: self.env['ir.sequence'].next_by_code( 'stock.quant.package') or _('Unknown Pack')) quant_ids = fields.One2many('stock.quant', 'package_id', 'Bulk Content', readonly=True) packaging_id = fields.Many2one( 'product.packaging', 'Package Type', index=True, help= "This field should be completed only if everything inside the package share the same product, otherwise it doesn't really makes sense." ) location_id = fields.Many2one('stock.location', 'Location', compute='_compute_package_info', search='_search_location', index=True, readonly=True) company_id = fields.Many2one('res.company', 'Company', compute='_compute_package_info', search='_search_company', index=True, readonly=True) owner_id = fields.Many2one('res.partner', 'Owner', compute='_compute_package_info', search='_search_owner', index=True, readonly=True) move_line_ids = fields.One2many('stock.move.line', 'result_package_id') current_picking_move_line_ids = fields.One2many( 'stock.move.line', compute="_compute_current_picking_info") current_picking_id = fields.Boolean( compute="_compute_current_picking_info") current_source_location_id = fields.Many2one( 'stock.location', compute="_compute_current_picking_info") current_destination_location_id = fields.Many2one( 'stock.location', compute="_compute_current_picking_info") is_processed = fields.Boolean(compute="_compute_current_picking_info") @api.depends('quant_ids.package_id', 'quant_ids.location_id', 'quant_ids.company_id', 'quant_ids.owner_id') def _compute_package_info(self): for package in self: values = { 'location_id': False, 'company_id': self.env.user.company_id.id, 'owner_id': False } if package.quant_ids: values['location_id'] = package.quant_ids[0].location_id package.location_id = values['location_id'] package.company_id = values['company_id'] package.owner_id = values['owner_id'] def name_get(self): return list(self._compute_complete_name().items()) def _compute_complete_name(self): """ Forms complete name of location from parent location to child location. """ res = {} for package in self: name = package.name res[package.id] = name return res def _compute_current_picking_info(self): """ When a package is in displayed in picking, it gets the picking id trough the context, and this function populates the different fields used when we move entire packages in pickings. """ for package in self: picking_id = self.env.context.get('picking_id') if not picking_id: package.current_picking_move_line_ids = False package.current_picking_id = False package.is_processed = False package.current_source_location_id = False package.current_destination_location_id = False continue package.current_picking_move_line_ids = package.move_line_ids.filtered( lambda ml: ml.picking_id.id == picking_id) package.current_picking_id = True package.current_source_location_id = package.current_picking_move_line_ids[: 1].location_id package.current_destination_location_id = package.current_picking_move_line_ids[: 1].location_dest_id package.is_processed = not bool( package.current_picking_move_line_ids.filtered( lambda ml: ml.qty_done < ml.product_uom_qty)) def action_toggle_processed(self): """ This method set the quantity done to the reserved quantity of all move lines of a package or to 0 if the package is already processed""" picking_id = self.env.context.get('picking_id') if picking_id: self.ensure_one() move_lines = self.current_picking_move_line_ids if move_lines.filtered( lambda ml: ml.qty_done < ml.product_uom_qty): destination_location = self.env.context.get( 'destination_location') for ml in move_lines: vals = {'qty_done': ml.product_uom_qty} if destination_location: vals['location_dest_id'] = destination_location ml.write(vals) else: for ml in move_lines: ml.qty_done = 0 def _search_location(self, operator, value): if value: packs = self.search([('quant_ids.location_id', operator, value)]) else: packs = self.search([('quant_ids', operator, value)]) if packs: return [('id', 'in', packs.ids)] else: return [('id', '=', False)] def _search_company(self, operator, value): if value: packs = self.search([('quant_ids.company_id', operator, value)]) else: packs = self.search([('quant_ids', operator, value)]) if packs: return [('id', 'parent_of', packs.ids)] else: return [('id', '=', False)] def _search_owner(self, operator, value): if value: packs = self.search([('quant_ids.owner_id', operator, value)]) else: packs = self.search([('quant_ids', operator, value)]) if packs: return [('id', 'parent_of', packs.ids)] else: return [('id', '=', False)] def _check_location_constraint(self): '''checks that all quants in a package are stored in the same location. This function cannot be used as a constraint because it needs to be checked on pack operations (they may not call write on the package) ''' for pack in self: locations = pack.get_content().filtered( lambda quant: quant.qty > 0.0).mapped('location_id') if len(locations) != 1: raise UserError( _('Everything inside a package should be in the same location' )) return True def unpack(self): for package in self: move_lines_to_remove = package.move_line_ids.filtered( lambda move_line: move_line.state != 'done') if move_lines_to_remove: move_lines_to_remove.write({'result_package_id': False}) else: package.mapped('quant_ids').write({'package_id': False}) def action_view_picking(self): action = self.env.ref('stock.action_picking_tree_all').read()[0] domain = [ '|', ('result_package_id', 'in', self.ids), ('package_id', 'in', self.ids) ] pickings = self.env['stock.move.line'].search(domain).mapped( 'picking_id') action['domain'] = [('id', 'in', pickings.ids)] return action def view_content_package(self): action = self.env['ir.actions.act_window'].for_xml_id( 'stock', 'quantsact') action['domain'] = [('id', 'in', self._get_contained_quants().ids)] return action def _get_contained_quants(self): return self.env['stock.quant'].search([('package_id', 'child_of', self.ids)]) def _get_all_products_quantities(self): '''This function computes the different product quantities for the given package ''' # TDE CLEANME: probably to move somewhere else, like in pack op res = {} for quant in self._get_contained_quants(): if quant.product_id not in res: res[quant.product_id] = 0 res[quant.product_id] += quant.qty return res
class Menu(models.Model): _name = "website.menu" _description = "Website Menu" _parent_store = True _parent_order = 'sequence' _order = "sequence, id" def _default_sequence(self): menu = self.search([], limit=1, order="sequence DESC") return menu.sequence or 0 name = fields.Char('Menu', required=True, translate=True) url = fields.Char('Url', default='') page_id = fields.Many2one('website.page', 'Related Page') new_window = fields.Boolean('New Window') sequence = fields.Integer(default=_default_sequence) website_id = fields.Many2one('website', 'Website', required=True, default=lambda self: self.env.ref('website.default_website')) # TODO: support multiwebsite once done for ir.ui.views parent_id = fields.Many2one('website.menu', 'Parent Menu', index=True, ondelete="cascade", domain="[('website_id','=', website_id)]") child_id = fields.One2many('website.menu', 'parent_id', string='Child Menus') parent_left = fields.Integer('Parent Left', index=True) parent_right = fields.Integer('Parent Rigth', index=True) is_visible = fields.Boolean(compute='_compute_visible', string='Is Visible') menu_view = fields.Many2one('ir.ui.view', domain=[('type', '=', 'qweb')], string='Menu View') @api.one def _compute_visible(self): visible = True if self.page_id and not self.page_id.sudo().is_visible and not self.user_has_groups('base.group_user'): visible = False self.is_visible = visible @api.model def clean_url(self): # clean the url with heuristic if self.page_id: url = self.page_id.sudo().url else: url = self.url if url and not self.url.startswith('/'): if '@' in self.url: if not self.url.startswith('mailto'): url = 'mailto:%s' % self.url elif not self.url.startswith('http'): url = '/%s' % self.url return url # would be better to take a menu_id as argument @api.model def get_tree(self, website_id, menu_id=None): def make_tree(node): page_id = node.page_id.id if node.page_id else None is_homepage = page_id and self.env['website'].browse(website_id).homepage_id.id == page_id menu_node = dict( id=node.id, name=node.name, url=node.page_id.url if page_id else node.url, new_window=node.new_window, sequence=node.sequence, parent_id=node.parent_id.id, children=[], is_homepage=is_homepage, ) for child in node.child_id: menu_node['children'].append(make_tree(child)) return menu_node if menu_id: menu = self.browse(menu_id) else: menu = self.env['website'].browse(website_id).menu_id return make_tree(menu) @api.model def save(self, website_id, data): def replace_id(old_id, new_id): for menu in data['data']: if menu['id'] == old_id: menu['id'] = new_id if menu['parent_id'] == old_id: menu['parent_id'] = new_id to_delete = data['to_delete'] if to_delete: self.browse(to_delete).unlink() for menu in data['data']: mid = menu['id'] # new menu are prefixed by new- if isinstance(mid, pycompat.string_types): new_menu = self.create({'name': menu['name']}) replace_id(mid, new_menu.id) for menu in data['data']: menu_id = self.browse(menu['id']) # if the url match a website.page, set the m2o relation page = self.env['website.page'].search([('url', '=', menu['url'])], limit=1) if page: menu['page_id'] = page.id elif menu_id.page_id: menu_id.page_id.write({'url': menu['url']}) if 'is_homepage' in menu: del menu['is_homepage'] if 'className' in menu: del menu['className'] if 'style' in menu: del menu['style'] if 'text' in menu: del menu['text'] menu_id.write(menu) return True
class Product(models.Model): _inherit = "product.product" stock_quant_ids = fields.One2many('stock.quant', 'product_id', help='Technical: used to compute quantities.') stock_move_ids = fields.One2many('stock.move', 'product_id', help='Technical: used to compute quantities.') qty_available = fields.Float( 'Quantity On Hand', compute='_compute_quantities', search='_search_qty_available', digits=dp.get_precision('Product Unit of Measure'), help="Current quantity of products.\n" "In a context with a single Stock Location, this includes " "goods stored at this Location, or any of its children.\n" "In a context with a single Warehouse, this includes " "goods stored in the Stock Location of this Warehouse, or any " "of its children.\n" "stored in the Stock Location of the Warehouse of this Shop, " "or any of its children.\n" "Otherwise, this includes goods stored in any Stock Location " "with 'internal' type.") virtual_available = fields.Float( 'Forecast Quantity', compute='_compute_quantities', search='_search_virtual_available', digits=dp.get_precision('Product Unit of Measure'), help="Forecast quantity (computed as Quantity On Hand " "- Outgoing + Incoming)\n" "In a context with a single Stock Location, this includes " "goods stored in this location, or any of its children.\n" "In a context with a single Warehouse, this includes " "goods stored in the Stock Location of this Warehouse, or any " "of its children.\n" "Otherwise, this includes goods stored in any Stock Location " "with 'internal' type.") incoming_qty = fields.Float( 'Incoming', compute='_compute_quantities', search='_search_incoming_qty', digits=dp.get_precision('Product Unit of Measure'), help="Quantity of planned incoming products.\n" "In a context with a single Stock Location, this includes " "goods arriving to this Location, or any of its children.\n" "In a context with a single Warehouse, this includes " "goods arriving to the Stock Location of this Warehouse, or " "any of its children.\n" "Otherwise, this includes goods arriving to any Stock " "Location with 'internal' type.") outgoing_qty = fields.Float( 'Outgoing', compute='_compute_quantities', search='_search_outgoing_qty', digits=dp.get_precision('Product Unit of Measure'), help="Quantity of planned outgoing products.\n" "In a context with a single Stock Location, this includes " "goods leaving this Location, or any of its children.\n" "In a context with a single Warehouse, this includes " "goods leaving the Stock Location of this Warehouse, or " "any of its children.\n" "Otherwise, this includes goods leaving any Stock " "Location with 'internal' type.") orderpoint_ids = fields.One2many('stock.warehouse.orderpoint', 'product_id', 'Minimum Stock Rules') nbr_reordering_rules = fields.Integer('Reordering Rules', compute='_compute_nbr_reordering_rules') reordering_min_qty = fields.Float(compute='_compute_nbr_reordering_rules') reordering_max_qty = fields.Float(compute='_compute_nbr_reordering_rules') @api.depends('stock_move_ids.product_qty', 'stock_move_ids.state') def _compute_quantities(self): res = self._compute_quantities_dict(self._context.get('lot_id'), self._context.get('owner_id'), self._context.get('package_id'), self._context.get('from_date'), self._context.get('to_date')) for product in self: product.qty_available = res[product.id]['qty_available'] product.incoming_qty = res[product.id]['incoming_qty'] product.outgoing_qty = res[product.id]['outgoing_qty'] product.virtual_available = res[product.id]['virtual_available'] def _product_available(self, field_names=None, arg=False): """ Compatibility method """ return self._compute_quantities_dict(self._context.get('lot_id'), self._context.get('owner_id'), self._context.get('package_id'), self._context.get('from_date'), self._context.get('to_date')) def _compute_quantities_dict(self, lot_id, owner_id, package_id, from_date=False, to_date=False): domain_quant_loc, domain_move_in_loc, domain_move_out_loc = self._get_domain_locations() domain_quant = [('product_id', 'in', self.ids)] + domain_quant_loc dates_in_the_past = False if to_date and to_date < fields.Datetime.now(): #Only to_date as to_date will correspond to qty_available dates_in_the_past = True domain_move_in = [('product_id', 'in', self.ids)] + domain_move_in_loc domain_move_out = [('product_id', 'in', self.ids)] + domain_move_out_loc if lot_id: domain_quant += [('lot_id', '=', lot_id)] if owner_id: domain_quant += [('owner_id', '=', owner_id)] domain_move_in += [('restrict_partner_id', '=', owner_id)] domain_move_out += [('restrict_partner_id', '=', owner_id)] if package_id: domain_quant += [('package_id', '=', package_id)] if dates_in_the_past: domain_move_in_done = list(domain_move_in) domain_move_out_done = list(domain_move_out) if from_date: domain_move_in += [('date', '>=', from_date)] domain_move_out += [('date', '>=', from_date)] if to_date: domain_move_in += [('date', '<=', to_date)] domain_move_out += [('date', '<=', to_date)] Move = self.env['stock.move'] Quant = self.env['stock.quant'] domain_move_in_todo = [('state', 'not in', ('done', 'cancel', 'draft'))] + domain_move_in domain_move_out_todo = [('state', 'not in', ('done', 'cancel', 'draft'))] + domain_move_out moves_in_res = dict((item['product_id'][0], item['product_qty']) for item in Move.read_group(domain_move_in_todo, ['product_id', 'product_qty'], ['product_id'], orderby='id')) moves_out_res = dict((item['product_id'][0], item['product_qty']) for item in Move.read_group(domain_move_out_todo, ['product_id', 'product_qty'], ['product_id'], orderby='id')) quants_res = dict((item['product_id'][0], item['quantity']) for item in Quant.read_group(domain_quant, ['product_id', 'quantity'], ['product_id'], orderby='id')) if dates_in_the_past: # Calculate the moves that were done before now to calculate back in time (as most questions will be recent ones) domain_move_in_done = [('state', '=', 'done'), ('date', '>', to_date)] + domain_move_in_done domain_move_out_done = [('state', '=', 'done'), ('date', '>', to_date)] + domain_move_out_done moves_in_res_past = dict((item['product_id'][0], item['product_qty']) for item in Move.read_group(domain_move_in_done, ['product_id', 'product_qty'], ['product_id'], orderby='id')) moves_out_res_past = dict((item['product_id'][0], item['product_qty']) for item in Move.read_group(domain_move_out_done, ['product_id', 'product_qty'], ['product_id'], orderby='id')) res = dict() for product in self.with_context(prefetch_fields=False): res[product.id] = {} if dates_in_the_past: qty_available = quants_res.get(product.id, 0.0) - moves_in_res_past.get(product.id, 0.0) + moves_out_res_past.get(product.id, 0.0) else: qty_available = quants_res.get(product.id, 0.0) res[product.id]['qty_available'] = float_round(qty_available, precision_rounding=product.uom_id.rounding) res[product.id]['incoming_qty'] = float_round(moves_in_res.get(product.id, 0.0), precision_rounding=product.uom_id.rounding) res[product.id]['outgoing_qty'] = float_round(moves_out_res.get(product.id, 0.0), precision_rounding=product.uom_id.rounding) res[product.id]['virtual_available'] = float_round( qty_available + res[product.id]['incoming_qty'] - res[product.id]['outgoing_qty'], precision_rounding=product.uom_id.rounding) return res def _get_domain_locations(self): ''' Parses the context and returns a list of location_ids based on it. It will return all stock locations when no parameters are given Possible parameters are shop, warehouse, location, force_company, compute_child ''' Warehouse = self.env['stock.warehouse'] if self.env.context.get('company_owned', False): company_id = self.env.user.company_id.id return ( [('location_id.company_id', '=', company_id)], [('location_id.company_id', '=', False), ('location_dest_id.company_id', '=', company_id)], [('location_id.company_id', '=', company_id), ('location_dest_id.company_id', '=', False), ]) location_ids = [] if self.env.context.get('location', False): if isinstance(self.env.context['location'], pycompat.integer_types): location_ids = [self.env.context['location']] elif isinstance(self.env.context['location'], pycompat.string_types): domain = [('complete_name', 'ilike', self.env.context['location'])] if self.env.context.get('force_company', False): domain += [('company_id', '=', self.env.context['force_company'])] location_ids = self.env['stock.location'].search(domain).ids else: location_ids = self.env.context['location'] else: if self.env.context.get('warehouse', False): if isinstance(self.env.context['warehouse'], pycompat.integer_types): wids = [self.env.context['warehouse']] elif isinstance(self.env.context['warehouse'], pycompat.string_types): domain = [('name', 'ilike', self.env.context['warehouse'])] if self.env.context.get('force_company', False): domain += [('company_id', '=', self.env.context['force_company'])] wids = Warehouse.search(domain).ids else: wids = self.env.context['warehouse'] else: wids = Warehouse.search([]).ids for w in Warehouse.browse(wids): location_ids.append(w.view_location_id.id) return self._get_domain_locations_new(location_ids, company_id=self.env.context.get('force_company', False), compute_child=self.env.context.get('compute_child', True)) def _get_domain_locations_new(self, location_ids, company_id=False, compute_child=True): operator = compute_child and 'child_of' or 'in' domain = company_id and ['&', ('company_id', '=', company_id)] or [] locations = self.env['stock.location'].browse(location_ids) # TDE FIXME: should move the support of child_of + auto_join directly in expression # The code has been modified because having one location with parent_left being # 0 make the whole domain unusable hierarchical_locations = locations.filtered(lambda location: location.parent_left != 0 and operator == "child_of") other_locations = locations.filtered(lambda location: location not in hierarchical_locations) # TDE: set - set ? loc_domain = [] dest_loc_domain = [] for location in hierarchical_locations: loc_domain = loc_domain and ['|'] + loc_domain or loc_domain loc_domain += ['&', ('location_id.parent_left', '>=', location.parent_left), ('location_id.parent_left', '<', location.parent_right)] dest_loc_domain = dest_loc_domain and ['|'] + dest_loc_domain or dest_loc_domain dest_loc_domain += ['&', ('location_dest_id.parent_left', '>=', location.parent_left), ('location_dest_id.parent_left', '<', location.parent_right)] if other_locations: loc_domain = loc_domain and ['|'] + loc_domain or loc_domain loc_domain = loc_domain + [('location_id', operator, [location.id for location in other_locations])] dest_loc_domain = dest_loc_domain and ['|'] + dest_loc_domain or dest_loc_domain dest_loc_domain = dest_loc_domain + [('location_dest_id', operator, [location.id for location in other_locations])] return ( domain + loc_domain, domain + dest_loc_domain + ['!'] + loc_domain if loc_domain else domain + dest_loc_domain, domain + loc_domain + ['!'] + dest_loc_domain if dest_loc_domain else domain + loc_domain ) def _search_virtual_available(self, operator, value): # TDE FIXME: should probably clean the search methods return self._search_product_quantity(operator, value, 'virtual_available') def _search_incoming_qty(self, operator, value): # TDE FIXME: should probably clean the search methods return self._search_product_quantity(operator, value, 'incoming_qty') def _search_outgoing_qty(self, operator, value): # TDE FIXME: should probably clean the search methods return self._search_product_quantity(operator, value, 'outgoing_qty') def _search_product_quantity(self, operator, value, field): # TDE FIXME: should probably clean the search methods # to prevent sql injections if field not in ('qty_available', 'virtual_available', 'incoming_qty', 'outgoing_qty'): raise UserError(_('Invalid domain left operand %s') % field) if operator not in ('<', '>', '=', '!=', '<=', '>='): raise UserError(_('Invalid domain operator %s') % operator) if not isinstance(value, (float, int)): raise UserError(_('Invalid domain right operand %s') % value) # TODO: Still optimization possible when searching virtual quantities ids = [] for product in self.search([]): if OPERATORS[operator](product[field], value): ids.append(product.id) return [('id', 'in', ids)] def _search_qty_available(self, operator, value): # TDE FIXME: should probably clean the search methods if value == 0.0 and operator in ('=', '>=', '<='): return self._search_product_quantity(operator, value, 'qty_available') product_ids = self._search_qty_available_new(operator, value, self._context.get('lot_id'), self._context.get('owner_id'), self._context.get('package_id')) if (value > 0 and operator in ('<=', '<')) or (value < 0 and operator in ('>=', '>')): # include also unavailable products domain = self._search_product_quantity(operator, value, 'qty_available') product_ids += domain[0][2] return [('id', 'in', product_ids)] def _search_qty_available_new(self, operator, value, lot_id=False, owner_id=False, package_id=False): # TDE FIXME: should probably clean the search methods product_ids = set() domain_quant = self._get_domain_locations()[0] if lot_id: domain_quant.append(('lot_id', '=', lot_id)) if owner_id: domain_quant.append(('owner_id', '=', owner_id)) if package_id: domain_quant.append(('package_id', '=', package_id)) quants_groupby = self.env['stock.quant'].read_group(domain_quant, ['product_id', 'quantity'], ['product_id'], orderby='id') for quant in quants_groupby: if OPERATORS[operator](quant['quantity'], value): product_ids.add(quant['product_id'][0]) return list(product_ids) def _compute_nbr_reordering_rules(self): read_group_res = self.env['stock.warehouse.orderpoint'].read_group( [('product_id', 'in', self.ids)], ['product_id', 'product_min_qty', 'product_max_qty'], ['product_id']) res = {i: {} for i in self.ids} for data in read_group_res: res[data['product_id'][0]]['nbr_reordering_rules'] = int(data['product_id_count']) res[data['product_id'][0]]['reordering_min_qty'] = data['product_min_qty'] res[data['product_id'][0]]['reordering_max_qty'] = data['product_max_qty'] for product in self: product.nbr_reordering_rules = res[product.id].get('nbr_reordering_rules', 0) product.reordering_min_qty = res[product.id].get('reordering_min_qty', 0) product.reordering_max_qty = res[product.id].get('reordering_max_qty', 0) @api.onchange('tracking') def onchange_tracking(self): products = self.filtered(lambda self: self.tracking and self.tracking != 'none') if products: unassigned_quants = self.env['stock.quant'].search_count([('product_id', 'in', products.ids), ('lot_id', '=', False), ('location_id.usage','=', 'internal')]) if unassigned_quants: return { 'warning': { 'title': _('Warning!'), 'message': _("You have products in stock that have no lot number. You can assign serial numbers by doing an inventory. ")}} @api.model def view_header_get(self, view_id, view_type): res = super(Product, self).view_header_get(view_id, view_type) if not res and self._context.get('active_id') and self._context.get('active_model') == 'stock.location': res = '%s%s' % (_('Products: '), self.env['stock.location'].browse(self._context['active_id']).name) return res @api.model def fields_view_get(self, view_id=None, view_type='form', toolbar=False, submenu=False): res = super(Product, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu) if self._context.get('location') and isinstance(self._context['location'], pycompat.integer_types): location = self.env['stock.location'].browse(self._context['location']) fields = res.get('fields') if fields: if location.usage == 'supplier': if fields.get('virtual_available'): res['fields']['virtual_available']['string'] = _('Future Receipts') if fields.get('qty_available'): res['fields']['qty_available']['string'] = _('Received Qty') elif location.usage == 'internal': if fields.get('virtual_available'): res['fields']['virtual_available']['string'] = _('Forecasted Quantity') elif location.usage == 'customer': if fields.get('virtual_available'): res['fields']['virtual_available']['string'] = _('Future Deliveries') if fields.get('qty_available'): res['fields']['qty_available']['string'] = _('Delivered Qty') elif location.usage == 'inventory': if fields.get('virtual_available'): res['fields']['virtual_available']['string'] = _('Future P&L') if fields.get('qty_available'): res['fields']['qty_available']['string'] = _('P&L Qty') elif location.usage == 'procurement': if fields.get('virtual_available'): res['fields']['virtual_available']['string'] = _('Future Qty') if fields.get('qty_available'): res['fields']['qty_available']['string'] = _('Unplanned Qty') elif location.usage == 'production': if fields.get('virtual_available'): res['fields']['virtual_available']['string'] = _('Future Productions') if fields.get('qty_available'): res['fields']['qty_available']['string'] = _('Produced Qty') return res def action_view_routes(self): return self.mapped('product_tmpl_id').action_view_routes() def action_view_stock_move_lines(self): self.ensure_one() action = self.env.ref('stock.stock_move_line_action').read()[0] action['domain'] = [('product_id', '=', self.id)] return action def action_open_product_lot(self): self.ensure_one() action = self.env.ref('stock.action_production_lot_form').read()[0] action['domain'] = [('product_id', '=', self.id)] action['context'] = {'default_product_id': self.id} return action def write(self, values): res = super(Product, self).write(values) if 'active' in values and not values['active']: products = self.mapped('orderpoint_ids').filtered(lambda r: r.active).mapped('product_id') if products: msg = _('You still have some active reordering rules on this product. Please archive or delete them first.') msg += '\n\n' for product in products: msg += '- %s \n' % product.display_name raise UserError(msg) return res
class MrpBomLine(models.Model): _name = 'mrp.bom.line' _order = "sequence, id" _rec_name = "product_id" def _get_default_product_uom_id(self): return self.env['product.uom'].search([], limit=1, order='id').id product_id = fields.Many2one('product.product', 'Product', required=True) product_qty = fields.Float( 'Product Quantity', default=1.0, digits=dp.get_precision('Product Unit of Measure'), required=True) product_uom_id = fields.Many2one( 'product.uom', 'Product Unit of Measure', default=_get_default_product_uom_id, oldname='product_uom', required=True, help= "Unit of Measure (Unit of Measure) is the unit of measurement for the inventory control" ) sequence = fields.Integer('Sequence', default=1, help="Gives the sequence order when displaying.") routing_id = fields.Many2one( 'mrp.routing', 'Routing', related='bom_id.routing_id', store=True, help= "The list of operations to produce the finished product. The routing is mainly used to " "compute work center costs during operations and to plan future loads on work centers " "based on production planning.") bom_id = fields.Many2one('mrp.bom', 'Parent BoM', index=True, ondelete='cascade', required=True) attribute_value_ids = fields.Many2many( 'product.attribute.value', string='Variants', help="BOM Product Variants needed form apply this line.") operation_id = fields.Many2one( 'mrp.routing.workcenter', 'Consumed in Operation', help= "The operation where the components are consumed, or the finished products created." ) child_bom_id = fields.Many2one('mrp.bom', 'Sub BoM', compute='_compute_child_bom_id') child_line_ids = fields.One2many('mrp.bom.line', string="BOM lines of the referred bom", compute='_compute_child_line_ids') has_attachments = fields.Boolean('Has Attachments', compute='_compute_has_attachments') _sql_constraints = [ ('bom_qty_zero', 'CHECK (product_qty>=0)', 'All product quantities must be greater or equal to 0.\n' 'Lines with 0 quantities can be used as optional lines. \n' 'You should install the mrp_byproduct module if you want to manage extra products on BoMs !' ), ] @api.one @api.depends('product_id', 'bom_id') def _compute_child_bom_id(self): if not self.product_id: self.child_bom_id = False else: self.child_bom_id = self.env['mrp.bom']._bom_find( product_tmpl=self.product_id.product_tmpl_id, product=self.product_id, picking_type=self.bom_id.picking_type_id) @api.one @api.depends('product_id') def _compute_has_attachments(self): nbr_attach = self.env['ir.attachment'].search_count([ '|', '&', ('res_model', '=', 'product.product'), ('res_id', '=', self.product_id.id), '&', ('res_model', '=', 'product.template'), ('res_id', '=', self.product_id.product_tmpl_id.id) ]) self.has_attachments = bool(nbr_attach) @api.one @api.depends('child_bom_id') def _compute_child_line_ids(self): """ If the BOM line refers to a BOM, return the ids of the child BOM lines """ self.child_line_ids = self.child_bom_id.bom_line_ids.ids @api.onchange('product_uom_id') def onchange_product_uom_id(self): res = {} if not self.product_uom_id or not self.product_id: return res if self.product_uom_id.category_id != self.product_id.uom_id.category_id: self.product_uom_id = self.product_id.uom_id.id res['warning'] = { 'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.' ) } return res @api.onchange('product_id') def onchange_product_id(self): if self.product_id: self.product_uom_id = self.product_id.uom_id.id @api.model def create(self, values): if 'product_id' in values and 'product_uom_id' not in values: values['product_uom_id'] = self.env['product.product'].browse( values['product_id']).uom_id.id return super(MrpBomLine, self).create(values) def _skip_bom_line(self, product): """ Control if a BoM line should be produce, can be inherited for add custom control. It currently checks that all variant values are in the product. """ if self.attribute_value_ids: if not product or self.attribute_value_ids - product.attribute_value_ids: return True return False @api.multi def action_see_attachments(self): domain = [ '|', '&', ('res_model', '=', 'product.product'), ('res_id', '=', self.product_id.id), '&', ('res_model', '=', 'product.template'), ('res_id', '=', self.product_id.product_tmpl_id.id) ] attachment_view = self.env.ref('mrp.view_document_file_kanban_mrp') return { 'name': _('Attachments'), 'domain': domain, 'res_model': 'mrp.document', 'type': 'ir.actions.act_window', 'view_id': attachment_view.id, 'views': [(attachment_view.id, 'kanban'), (False, 'form')], 'view_mode': 'kanban,tree,form', 'view_type': 'form', 'help': _('''<p class="oe_view_nocontent_create"> Click to upload files to your product. </p><p> Use this feature to store any files, like drawings or specifications. </p>'''), 'limit': 80, 'context': "{'default_res_model': '%s','default_res_id': %d}" % ('product.product', self.product_id.id) }
class AccountAnalyticAccount(models.Model): _inherit = "account.analytic.account" crossovered_budget_line = fields.One2many('crossovered.budget.lines', 'analytic_account_id', 'Budget Lines')
class Picking(models.Model): _name = "stock.picking" _inherit = ['mail.thread', 'mail.activity.mixin'] _description = "Transfer" _order = "priority desc, date asc, id desc" name = fields.Char( 'Reference', default='/', copy=False, index=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) origin = fields.Char( 'Source Document', index=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, help="Reference of the document") note = fields.Text('Notes') backorder_id = fields.Many2one( 'stock.picking', 'Back Order of', copy=False, index=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, help="If this shipment was split, then this field links to the shipment which contains the already processed part.") move_type = fields.Selection([ ('direct', 'As soon as possible'), ('one', 'When all products are ready')], 'Shipping Policy', default='direct', required=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, help="It specifies goods to be deliver partially or all at once") state = fields.Selection([ ('draft', 'Draft'), ('waiting', 'Waiting Another Operation'), ('confirmed', 'Waiting'), ('assigned', 'Ready'), ('done', 'Done'), ('cancel', 'Cancelled'), ], string='Status', compute='_compute_state', copy=False, index=True, readonly=True, store=True, track_visibility='onchange', help=" * Draft: not confirmed yet and will not be scheduled until confirmed.\n" " * Waiting Another Operation: waiting for another move to proceed before it becomes automatically available (e.g. in Make-To-Order flows).\n" " * Waiting: if it is not ready to be sent because the required products could not be reserved.\n" " * Ready: products are reserved and ready to be sent. If the shipping policy is 'As soon as possible' this happens as soon as anything is reserved.\n" " * Done: has been processed, can't be modified or cancelled anymore.\n" " * Cancelled: has been cancelled, can't be confirmed anymore.") group_id = fields.Many2one( 'procurement.group', 'Procurement Group', readonly=True, related='move_lines.group_id', store=True) priority = fields.Selection( PROCUREMENT_PRIORITIES, string='Priority', compute='_compute_priority', inverse='_set_priority', store=True, # default='1', required=True, # TDE: required, depending on moves ? strange index=True, track_visibility='onchange', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, help="Priority for this picking. Setting manually a value here would set it as priority for all the moves") scheduled_date = fields.Datetime( 'Scheduled Date', compute='_compute_scheduled_date', inverse='_set_scheduled_date', store=True, index=True, track_visibility='onchange', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, help="Scheduled time for the first part of the shipment to be processed. Setting manually a value here would set it as expected date for all the stock moves.") date = fields.Datetime( 'Creation Date', default=fields.Datetime.now, index=True, track_visibility='onchange', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, help="Creation Date, usually the time of the order") date_done = fields.Datetime('Date of Transfer', copy=False, readonly=True, help="Completion Date of Transfer") location_id = fields.Many2one( 'stock.location', "Source Location", default=lambda self: self.env['stock.picking.type'].browse(self._context.get('default_picking_type_id')).default_location_src_id, readonly=True, required=True, states={'draft': [('readonly', False)]}) location_dest_id = fields.Many2one( 'stock.location', "Destination Location", default=lambda self: self.env['stock.picking.type'].browse(self._context.get('default_picking_type_id')).default_location_dest_id, readonly=True, required=True, states={'draft': [('readonly', False)]}) move_lines = fields.One2many('stock.move', 'picking_id', string="Stock Moves", copy=True) has_scrap_move = fields.Boolean( 'Has Scrap Moves', compute='_has_scrap_move') picking_type_id = fields.Many2one( 'stock.picking.type', 'Operation Type', required=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) picking_type_code = fields.Selection([ ('incoming', 'Vendors'), ('outgoing', 'Customers'), ('internal', 'Internal')], related='picking_type_id.code', readonly=True) picking_type_entire_packs = fields.Boolean(related='picking_type_id.show_entire_packs', readonly=True) partner_id = fields.Many2one( 'res.partner', 'Partner', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) company_id = fields.Many2one( 'res.company', 'Company', default=lambda self: self.env['res.company']._company_default_get('stock.picking'), index=True, required=True, states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) branch_id = fields.Many2one('res.branch', 'Branch', ondelete="restrict", default=lambda self: self.env['res.users']._get_default_branch(), states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}) move_line_ids = fields.One2many('stock.move.line', 'picking_id', 'Operations') move_line_exist = fields.Boolean( 'Has Pack Operations', compute='_compute_move_line_exist', help='Check the existence of pack operation on the picking') has_packages = fields.Boolean( 'Has Packages', compute='_compute_has_packages', help='Check the existence of destination packages on move lines') entire_package_ids = fields.One2many('stock.quant.package', compute='_compute_entire_package_ids', help='Those are the entire packages of a picking shown in the view of operations') entire_package_detail_ids = fields.One2many('stock.quant.package', compute='_compute_entire_package_ids', help='Those are the entire packages of a picking shown in the view of detailed operations') show_check_availability = fields.Boolean( compute='_compute_show_check_availability', help='Technical field used to compute whether the check availability button should be shown.') show_mark_as_todo = fields.Boolean( compute='_compute_show_mark_as_todo', help='Technical field used to compute whether the mark as todo button should be shown.') show_validate = fields.Boolean( compute='_compute_show_validate', help='Technical field used to compute whether the validate should be shown.') owner_id = fields.Many2one( 'res.partner', 'Owner', states={'done': [('readonly', True)], 'cancel': [('readonly', True)]}, help="Default Owner") printed = fields.Boolean('Printed') is_locked = fields.Boolean(default=True, help='When the picking is not done this allows changing the ' 'initial demand. When the picking is done this allows ' 'changing the done quantities.') # Used to search on pickings product_id = fields.Many2one('product.product', 'Product', related='move_lines.product_id') show_operations = fields.Boolean(compute='_compute_show_operations') show_lots_text = fields.Boolean(compute='_compute_show_lots_text') has_tracking = fields.Boolean(compute='_compute_has_tracking') _sql_constraints = [ ('name_uniq', 'unique(name, company_id)', 'Reference must be unique per company!'), ] def _compute_has_tracking(self): for picking in self: picking.has_tracking = any(m.has_tracking != 'none' for m in picking.move_lines) @api.depends('picking_type_id.show_operations') def _compute_show_operations(self): for picking in self: if self.env.context.get('force_detailed_view'): picking.show_operations = True break if picking.picking_type_id.show_operations: if (picking.state == 'draft' and not self.env.context.get('planned_picking')) or picking.state != 'draft': picking.show_operations = True else: picking.show_operations = False else: picking.show_operations = False @api.depends('move_line_ids', 'picking_type_id.use_create_lots', 'picking_type_id.use_existing_lots', 'state') def _compute_show_lots_text(self): group_production_lot_enabled = self.user_has_groups('stock.group_production_lot') for picking in self: if not picking.move_line_ids: picking.show_lots_text = False elif group_production_lot_enabled and picking.picking_type_id.use_create_lots \ and not picking.picking_type_id.use_existing_lots and picking.state != 'done': picking.show_lots_text = True else: picking.show_lots_text = False @api.depends('move_type', 'move_lines.state', 'move_lines.picking_id') @api.one def _compute_state(self): ''' State of a picking depends on the state of its related stock.move - Draft: only used for "planned pickings" - Waiting: if the picking is not ready to be sent so if - (a) no quantity could be reserved at all or if - (b) some quantities could be reserved and the shipping policy is "deliver all at once" - Waiting another move: if the picking is waiting for another move - Ready: if the picking is ready to be sent so if: - (a) all quantities are reserved or if - (b) some quantities could be reserved and the shipping policy is "as soon as possible" - Done: if the picking is done. - Cancelled: if the picking is cancelled ''' if not self.move_lines: self.state = 'draft' elif any(move.state == 'draft' for move in self.move_lines): # TDE FIXME: should be all ? self.state = 'draft' elif all(move.state == 'cancel' for move in self.move_lines): self.state = 'cancel' elif all(move.state in ['cancel', 'done'] for move in self.move_lines): self.state = 'done' else: relevant_move_state = self.move_lines._get_relevant_state_among_moves() if relevant_move_state == 'partially_available': self.state = 'assigned' else: self.state = relevant_move_state @api.one @api.depends('move_lines.priority') def _compute_priority(self): if self.mapped('move_lines'): priorities = [priority for priority in self.mapped('move_lines.priority') if priority] or ['1'] self.priority = max(priorities) else: self.priority = '1' @api.one def _set_priority(self): self.move_lines.write({'priority': self.priority}) @api.one @api.depends('move_lines.date_expected') def _compute_scheduled_date(self): if self.move_type == 'direct': self.scheduled_date = min(self.move_lines.mapped('date_expected') or [fields.Datetime.now()]) else: self.scheduled_date = max(self.move_lines.mapped('date_expected') or [fields.Datetime.now()]) @api.one def _set_scheduled_date(self): self.move_lines.write({'date_expected': self.scheduled_date}) @api.one def _has_scrap_move(self): # TDE FIXME: better implementation self.has_scrap_move = bool(self.env['stock.move'].search_count([('picking_id', '=', self.id), ('scrapped', '=', True)])) @api.one def _compute_move_line_exist(self): self.move_line_exist = bool(self.move_line_ids) @api.one def _compute_has_packages(self): self.has_packages = self.move_line_ids.filtered(lambda ml: ml.result_package_id) def _compute_entire_package_ids(self): """ This compute method populate the two one2Many containing all entire packages of the picking. An entire package is a package that is entirely reserved to be moved from a location to another one. """ for picking in self: packages = self.env['stock.quant.package'] packages_to_check = picking.move_line_ids\ .filtered(lambda ml: ml.result_package_id and ml.package_id.id == ml.result_package_id.id)\ .mapped('package_id') for package_to_check in packages_to_check: if picking.state in ('done', 'cancel') or picking._check_move_lines_map_quant_package(package_to_check): packages |= package_to_check picking.entire_package_ids = packages picking.entire_package_detail_ids = packages @api.multi def _compute_show_check_availability(self): for picking in self: has_moves_to_reserve = any( move.state in ('waiting', 'confirmed', 'partially_available') and float_compare(move.product_uom_qty, 0, precision_rounding=move.product_uom.rounding) for move in picking.move_lines ) picking.show_check_availability = picking.is_locked and picking.state in ('confirmed', 'waiting', 'assigned') and has_moves_to_reserve @api.multi @api.depends('state', 'move_lines') def _compute_show_mark_as_todo(self): for picking in self: if not picking.move_lines: picking.show_mark_as_todo = False elif self._context.get('planned_picking') and picking.state == 'draft': picking.show_mark_as_todo = True elif picking.state != 'draft' or not picking.id: picking.show_mark_as_todo = False else: picking.show_mark_as_todo = True @api.multi @api.depends('state', 'is_locked') def _compute_show_validate(self): for picking in self: if self._context.get('planned_picking') and picking.state == 'draft': picking.show_validate = False elif picking.state not in ('draft', 'waiting', 'confirmed', 'assigned') or not picking.is_locked: picking.show_validate = False else: picking.show_validate = True @api.onchange('picking_type_id', 'partner_id') def onchange_picking_type(self): if self.picking_type_id: if self.picking_type_id.default_location_src_id: location_id = self.picking_type_id.default_location_src_id.id elif self.partner_id: location_id = self.partner_id.property_stock_supplier.id else: customerloc, location_id = self.env['stock.warehouse']._get_partner_locations() if self.picking_type_id.default_location_dest_id: location_dest_id = self.picking_type_id.default_location_dest_id.id elif self.partner_id: location_dest_id = self.partner_id.property_stock_customer.id else: location_dest_id, supplierloc = self.env['stock.warehouse']._get_partner_locations() self.location_id = location_id self.location_dest_id = location_dest_id # TDE CLEANME move into onchange_partner_id if self.partner_id: if self.partner_id.picking_warn == 'no-message' and self.partner_id.parent_id: partner = self.partner_id.parent_id elif self.partner_id.picking_warn not in ('no-message', 'block') and self.partner_id.parent_id.picking_warn == 'block': partner = self.partner_id.parent_id else: partner = self.partner_id if partner.picking_warn != 'no-message': if partner.picking_warn == 'block': self.partner_id = False return {'warning': { 'title': ("Warning for %s") % partner.name, 'message': partner.picking_warn_msg }} @api.model def create(self, vals): # TDE FIXME: clean that brol defaults = self.default_get(['name', 'picking_type_id']) if vals.get('name', '/') == '/' and defaults.get('name', '/') == '/' and vals.get('picking_type_id', defaults.get('picking_type_id')): vals['name'] = self.env['stock.picking.type'].browse(vals.get('picking_type_id', defaults.get('picking_type_id'))).sequence_id.next_by_id() # TDE FIXME: what ? # As the on_change in one2many list is WIP, we will overwrite the locations on the stock moves here # As it is a create the format will be a list of (0, 0, dict) if vals.get('move_lines') and vals.get('location_id') and vals.get('location_dest_id'): for move in vals['move_lines']: if len(move) == 3: move[2]['location_id'] = vals['location_id'] move[2]['location_dest_id'] = vals['location_dest_id'] res = super(Picking, self).create(vals) res._autoconfirm_picking() return res @api.multi def write(self, vals): res = super(Picking, self).write(vals) # Change locations of moves if those of the picking change after_vals = {} if vals.get('location_id'): after_vals['location_id'] = vals['location_id'] if vals.get('location_dest_id'): after_vals['location_dest_id'] = vals['location_dest_id'] if after_vals: self.mapped('move_lines').filtered(lambda move: not move.scrapped).write(after_vals) if vals.get('move_lines'): # Do not run autoconfirm if any of the moves has an initial demand. If an initial demand # is present in any of the moves, it means the picking was created through the "planned # transfer" mechanism. pickings_to_not_autoconfirm = self.env['stock.picking'] for picking in self: if picking.state != 'draft': continue for move in picking.move_lines: if not float_is_zero(move.product_uom_qty, precision_rounding=move.product_uom.rounding): pickings_to_not_autoconfirm |= picking break (self - pickings_to_not_autoconfirm)._autoconfirm_picking() return res @api.multi def unlink(self): self.mapped('move_lines')._action_cancel() self.mapped('move_lines').unlink() # Checks if moves are not done return super(Picking, self).unlink() # Actions # ---------------------------------------- @api.one def action_assign_owner(self): self.move_line_ids.write({'owner_id': self.owner_id.id}) @api.multi def do_print_picking(self): self.write({'printed': True}) return self.env.ref('stock.action_report_picking').report_action(self) @api.multi def action_confirm(self): # call `_action_confirm` on every draft move self.mapped('move_lines')\ .filtered(lambda move: move.state == 'draft')\ ._action_confirm() # call `_action_assign` on every confirmed move which location_id bypasses the reservation self.filtered(lambda picking: picking.location_id.usage in ('supplier', 'inventory', 'production') and picking.state == 'confirmed')\ .mapped('move_lines')._action_assign() if self.env.context.get('planned_picking') and len(self) == 1: action = self.env.ref('stock.action_picking_form') result = action.read()[0] result['res_id'] = self.id result['context'] = { 'search_default_picking_type_id': [self.picking_type_id.id], 'default_picking_type_id': self.picking_type_id.id, 'contact_display': 'partner_address', 'planned_picking': False, } return result else: return True @api.multi def action_assign(self): """ Check availability of picking moves. This has the effect of changing the state and reserve quants on available moves, and may also impact the state of the picking as it is computed based on move's states. @return: True """ self.filtered(lambda picking: picking.state == 'draft').action_confirm() moves = self.mapped('move_lines').filtered(lambda move: move.state not in ('draft', 'cancel', 'done')) if not moves: raise UserError(_('Nothing to check the availability for.')) moves._action_assign() return True @api.multi def force_assign(self): """ Changes state of picking to available if moves are confirmed or waiting. @return: True """ self.mapped('move_lines').filtered(lambda move: move.state in ['confirmed', 'waiting', 'partially_available'])._force_assign() return True @api.multi def action_cancel(self): self.mapped('move_lines')._action_cancel() self.write({'is_locked': True}) return True @api.multi def action_done(self): """Changes picking state to done by processing the Stock Moves of the Picking Normally that happens when the button "Done" is pressed on a Picking view. @return: True """ # TDE FIXME: remove decorator when migration the remaining todo_moves = self.mapped('move_lines').filtered(lambda self: self.state in ['draft', 'waiting', 'partially_available', 'assigned', 'confirmed']) # Check if there are ops not linked to moves yet for pick in self: # # Explode manually added packages # for ops in pick.move_line_ids.filtered(lambda x: not x.move_id and not x.product_id): # for quant in ops.package_id.quant_ids: #Or use get_content for multiple levels # self.move_line_ids.create({'product_id': quant.product_id.id, # 'package_id': quant.package_id.id, # 'result_package_id': ops.result_package_id, # 'lot_id': quant.lot_id.id, # 'owner_id': quant.owner_id.id, # 'product_uom_id': quant.product_id.uom_id.id, # 'product_qty': quant.qty, # 'qty_done': quant.qty, # 'location_id': quant.location_id.id, # Could be ops too # 'location_dest_id': ops.location_dest_id.id, # 'picking_id': pick.id # }) # Might change first element # # Link existing moves or add moves when no one is related for ops in pick.move_line_ids.filtered(lambda x: not x.move_id): # Search move with this product moves = pick.move_lines.filtered(lambda x: x.product_id == ops.product_id) if moves: #could search move that needs it the most (that has some quantities left) ops.move_id = moves[0].id else: new_move = self.env['stock.move'].create({ 'name': _('New Move:') + ops.product_id.display_name, 'product_id': ops.product_id.id, 'product_uom_qty': ops.qty_done, 'product_uom': ops.product_uom_id.id, 'location_id': pick.location_id.id, 'location_dest_id': pick.location_dest_id.id, 'picking_id': pick.id, }) ops.move_id = new_move.id new_move._action_confirm() todo_moves |= new_move #'qty_done': ops.qty_done}) todo_moves._action_done() self.write({'date_done': fields.Datetime.now()}) return True # Backward compatibility # Problem with fixed reference to a function: # it doesn't allow for overriding action_done() through do_transfer # get rid of me in master (and make me private ?) def do_transfer(self): return self.action_done() def _check_move_lines_map_quant_package(self, package): """ This method checks that all product of the package (quant) are well present in the move_line_ids of the picking. """ all_in = True pack_move_lines = self.move_line_ids.filtered(lambda ml: ml.package_id == package) keys = ['product_id', 'lot_id'] grouped_quants = {} for k, g in groupby(sorted(package.quant_ids, key=itemgetter(*keys)), key=itemgetter(*keys)): grouped_quants[k] = sum(self.env['stock.quant'].concat(*list(g)).mapped('quantity')) grouped_ops = {} for k, g in groupby(sorted(pack_move_lines, key=itemgetter(*keys)), key=itemgetter(*keys)): grouped_ops[k] = sum(self.env['stock.move.line'].concat(*list(g)).mapped('product_qty')) if any(grouped_quants.get(key, 0) - grouped_ops.get(key, 0) != 0 for key in grouped_quants) \ or any(grouped_ops.get(key, 0) - grouped_quants.get(key, 0) != 0 for key in grouped_ops): all_in = False return all_in @api.multi def _check_entire_pack(self): """ This function check if entire packs are moved in the picking""" for picking in self: origin_packages = picking.move_line_ids.mapped("package_id") for pack in origin_packages: if picking._check_move_lines_map_quant_package(pack): picking.move_line_ids.filtered(lambda ml: ml.package_id == pack).write({'result_package_id': pack.id}) @api.multi def do_unreserve(self): for picking in self: picking.move_lines._do_unreserve() @api.multi def button_validate(self): self.ensure_one() if not self.move_lines and not self.move_line_ids: raise UserError(_('Please add some lines to move')) # If no lots when needed, raise error picking_type = self.picking_type_id no_quantities_done = all(float_is_zero(move_line.qty_done, precision_rounding=move_line.product_uom_id.rounding) for move_line in self.move_line_ids) no_reserved_quantities = all(float_is_zero(move_line.product_qty, precision_rounding=move_line.product_uom_id.rounding) for move_line in self.move_line_ids) if no_reserved_quantities and no_quantities_done: raise UserError(_('You cannot validate a transfer if you have not processed any quantity. You should rather cancel the transfer.')) if picking_type.use_create_lots or picking_type.use_existing_lots: lines_to_check = self.move_line_ids if not no_quantities_done: lines_to_check = lines_to_check.filtered( lambda line: float_compare(line.qty_done, 0, precision_rounding=line.product_uom_id.rounding) ) for line in lines_to_check: product = line.product_id if product and product.tracking != 'none': if not line.lot_name and not line.lot_id: raise UserError(_('You need to supply a lot/serial number for %s.') % product.display_name) elif line.qty_done == 0: raise UserError(_('You cannot validate a transfer if you have not processed any quantity for %s.') % product.display_name) if no_quantities_done: view = self.env.ref('stock.view_immediate_transfer') wiz = self.env['stock.immediate.transfer'].create({'pick_ids': [(4, self.id)]}) return { 'name': _('Immediate Transfer?'), 'type': 'ir.actions.act_window', 'view_type': 'form', 'view_mode': 'form', 'res_model': 'stock.immediate.transfer', 'views': [(view.id, 'form')], 'view_id': view.id, 'target': 'new', 'res_id': wiz.id, 'context': self.env.context, } if self._get_overprocessed_stock_moves() and not self._context.get('skip_overprocessed_check'): view = self.env.ref('stock.view_overprocessed_transfer') wiz = self.env['stock.overprocessed.transfer'].create({'picking_id': self.id}) return { 'type': 'ir.actions.act_window', 'view_type': 'form', 'view_mode': 'form', 'res_model': 'stock.overprocessed.transfer', 'views': [(view.id, 'form')], 'view_id': view.id, 'target': 'new', 'res_id': wiz.id, 'context': self.env.context, } # Check backorder should check for other barcodes if self._check_backorder(): return self.action_generate_backorder_wizard() self.action_done() return def action_generate_backorder_wizard(self): view = self.env.ref('stock.view_backorder_confirmation') wiz = self.env['stock.backorder.confirmation'].create({'pick_ids': [(4, p.id) for p in self]}) return { 'name': _('Create Backorder?'), 'type': 'ir.actions.act_window', 'view_type': 'form', 'view_mode': 'form', 'res_model': 'stock.backorder.confirmation', 'views': [(view.id, 'form')], 'view_id': view.id, 'target': 'new', 'res_id': wiz.id, 'context': self.env.context, } def action_toggle_is_locked(self): self.ensure_one() self.is_locked = not self.is_locked return True def _check_backorder(self): """ This method will loop over all the move lines of self and check if creating a backorder is necessary. This method is called during button_validate if the user has already processed some quantities and in the immediate transfer wizard that is displayed if the user has not processed any quantities. :return: True if a backorder is necessary else False """ quantity_todo = {} quantity_done = {} for move in self.mapped('move_lines'): quantity_todo.setdefault(move.product_id.id, 0) quantity_done.setdefault(move.product_id.id, 0) quantity_todo[move.product_id.id] += move.product_uom_qty quantity_done[move.product_id.id] += move.quantity_done for ops in self.mapped('move_line_ids').filtered(lambda x: x.package_id and not x.product_id and not x.move_id): for quant in ops.package_id.quant_ids: quantity_done.setdefault(quant.product_id.id, 0) quantity_done[quant.product_id.id] += quant.qty for pack in self.mapped('move_line_ids').filtered(lambda x: x.product_id and not x.move_id): quantity_done.setdefault(pack.product_id.id, 0) quantity_done[pack.product_id.id] += pack.qty_done return any(quantity_done[x] < quantity_todo.get(x, 0) for x in quantity_done) @api.multi def _autoconfirm_picking(self): if not self._context.get('planned_picking'): for picking in self.filtered(lambda picking: picking.state not in ('done', 'cancel') and picking.move_lines): picking.action_confirm() def _get_overprocessed_stock_moves(self): self.ensure_one() return self.move_lines.filtered( lambda move: move.product_uom_qty != 0 and float_compare(move.quantity_done, move.product_uom_qty, precision_rounding=move.product_uom.rounding) == 1 ) @api.multi def _create_backorder(self, backorder_moves=[]): """ Move all non-done lines into a new backorder picking. """ backorders = self.env['stock.picking'] for picking in self: moves_to_backorder = picking.move_lines.filtered(lambda x: x.state not in ('done', 'cancel')) if moves_to_backorder: backorder_picking = picking.copy({ 'name': '/', 'move_lines': [], 'move_line_ids': [], 'backorder_id': picking.id }) picking.message_post( _('The backorder <a href=# data-oe-model=stock.picking data-oe-id=%d>%s</a> has been created.') % ( backorder_picking.id, backorder_picking.name)) moves_to_backorder.write({'picking_id': backorder_picking.id}) moves_to_backorder.mapped('move_line_ids').write({'picking_id': backorder_picking.id}) backorder_picking.action_assign() backorders |= backorder_picking return backorders def _put_in_pack(self): package = False for pick in self.filtered(lambda p: p.state not in ('done', 'cancel')): operations = pick.move_line_ids.filtered(lambda o: o.qty_done > 0 and not o.result_package_id) operation_ids = self.env['stock.move.line'] if operations: package = self.env['stock.quant.package'].create({}) for operation in operations: if float_compare(operation.qty_done, operation.product_uom_qty, precision_rounding=operation.product_uom_id.rounding) >= 0: operation_ids |= operation else: quantity_left_todo = float_round( operation.product_uom_qty - operation.qty_done, precision_rounding=operation.product_uom_id.rounding, rounding_method='UP') done_to_keep = operation.qty_done new_operation = operation.copy( default={'product_uom_qty': 0, 'qty_done': operation.qty_done}) operation.write({'product_uom_qty': quantity_left_todo, 'qty_done': 0.0}) new_operation.write({'product_uom_qty': done_to_keep}) operation_ids |= new_operation operation_ids.write({'result_package_id': package.id}) else: raise UserError(_('Please process some quantities to put in the pack first!')) return package def put_in_pack(self): return self._put_in_pack() def button_scrap(self): self.ensure_one() products = self.env['product.product'] for move in self.move_lines: if move.state not in ('draft', 'cancel') and move.product_id.type in ('product', 'consu'): products |= move.product_id 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_picking_id': self.id, 'product_ids': products.ids}, 'target': 'new', } def action_see_move_scrap(self): self.ensure_one() action = self.env.ref('stock.action_stock_scrap').read()[0] scraps = self.env['stock.scrap'].search([('picking_id', '=', self.id)]) action['domain'] = [('id', 'in', scraps.ids)] return action def action_see_packages(self): self.ensure_one() action = self.env.ref('stock.action_package_view').read()[0] packages = self.move_line_ids.mapped('result_package_id') action['domain'] = [('id', 'in', packages.ids)] action['context'] = {'picking_id': self.id} return action
class Location(models.Model): _name = "stock.location" _description = "Inventory Locations" _parent_name = "location_id" _parent_store = True _parent_order = 'name' _order = 'parent_left' _rec_name = 'complete_name' @api.model def default_get(self, fields): res = super(Location, self).default_get(fields) if 'barcode' in fields and 'barcode' not in res and res.get( 'complete_name'): res['barcode'] = res['complete_name'] return res def _should_be_valued(self): self.ensure_one() if self.usage == 'internal' or (self.usage == 'transit' and self.company_id): return True return False name = fields.Char('Location Name', required=True, translate=True) complete_name = fields.Char("Full Location Name", compute='_compute_complete_name', store=True) active = fields.Boolean( 'Active', default=True, help= "By unchecking the active field, you may hide a location without deleting it." ) usage = fields.Selection( [('supplier', 'Vendor Location'), ('view', 'View'), ('internal', 'Internal Location'), ('customer', 'Customer Location'), ('inventory', 'Inventory Loss'), ('procurement', 'Procurement'), ('production', 'Production'), ('transit', 'Transit Location')], string='Location Type', default='internal', index=True, required=True, help= "* Vendor Location: Virtual location representing the source location for products coming from your vendors" "\n* View: Virtual location used to create a hierarchical structures for your warehouse, aggregating its child locations ; can't directly contain products" "\n* Internal Location: Physical locations inside your own warehouses," "\n* Customer Location: Virtual location representing the destination location for products sent to your customers" "\n* Inventory Loss: Virtual location serving as counterpart for inventory operations used to correct stock levels (Physical inventories)" "\n* Procurement: Virtual location serving as temporary counterpart for procurement operations when the source (vendor or production) is not known yet. This location should be empty when the procurement scheduler has finished running." "\n* Production: Virtual counterpart location for production operations: this location consumes the raw material and produces finished products" "\n* Transit Location: Counterpart location that should be used in inter-company or inter-warehouses operations" ) location_id = fields.Many2one( 'stock.location', 'Parent Location', index=True, ondelete='cascade', help= "The parent location that includes this location. Example : The 'Dispatch Zone' is the 'Gate 1' parent location." ) child_ids = fields.One2many('stock.location', 'location_id', 'Contains') partner_id = fields.Many2one('res.partner', 'Owner', help="Owner of the location if not internal") comment = fields.Text('Additional Information') posx = fields.Integer( 'Corridor (X)', default=0, help="Optional localization details, for information purpose only") posy = fields.Integer( 'Shelves (Y)', default=0, help="Optional localization details, for information purpose only") posz = fields.Integer( 'Height (Z)', default=0, help="Optional localization details, for information purpose only") parent_left = fields.Integer('Left Parent', index=True) parent_right = fields.Integer('Right Parent', index=True) company_id = fields.Many2one( 'res.company', 'Company', default=lambda self: self.env['res.company']._company_default_get( 'stock.location'), index=True, help='Let this field empty if this location is shared between companies' ) scrap_location = fields.Boolean( 'Is a Scrap Location?', default=False, help= 'Check this box to allow using this location to put scrapped/damaged goods.' ) return_location = fields.Boolean( 'Is a Return Location?', help='Check this box to allow using this location as a return location.' ) removal_strategy_id = fields.Many2one( 'product.removal', 'Removal Strategy', help= "Defines the default method used for suggesting the exact location (shelf) where to take the products from, which lot etc. for this location. This method can be enforced at the product category level, and a fallback is made on the parent locations if none is set here." ) putaway_strategy_id = fields.Many2one( 'product.putaway', 'Put Away Strategy', help= "Defines the default method used for suggesting the exact location (shelf) where to store the products. This method can be enforced at the product category level, and a fallback is made on the parent locations if none is set here." ) barcode = fields.Char('Barcode', copy=False, oldname='loc_barcode') branch_id = fields.Many2one('res.branch', 'Branch', ondelete="restrict") quant_ids = fields.One2many('stock.quant', 'location_id') _sql_constraints = [ ('barcode_company_uniq', 'unique (barcode,company_id)', 'The barcode for a location must be unique per company !') ] @api.one @api.depends('name', 'location_id.complete_name') def _compute_complete_name(self): """ Forms complete name of location from parent location to child location. """ if self.location_id.complete_name: self.complete_name = '%s/%s' % (self.location_id.complete_name, self.name) else: self.complete_name = self.name def write(self, values): if 'usage' in values and values['usage'] == 'view': if self.mapped('quant_ids'): raise UserError( _("This location's usage cannot be changed to view as it contains products." )) return super(Location, self).write(values) @api.multi @api.constrains('branch_id', 'location_id') def _check_parent_branch(self): for record in self: if (record.location_id and record.location_id.usage == 'internal' and record.branch_id and record.branch_id != record.location_id.branch_id): raise UserError( _('Configuration Error of Branch:\n' 'The Location Branch (%s) and ' 'the Branch (%s) of Parent Location must ' 'be the same branch!') % (recordord.branch_id.name, recordord.location_id.branch_id.name)) @api.multi @api.constrains('branch_id') def _check_warehouse_branch(self): for record in self: warehouse_obj = self.env['stock.warehouse'] warehouses_ids = warehouse_obj.search([ '|', '|', ('wh_input_stock_loc_id', '=', record.ids[0]), ('lot_stock_id', 'in', record.ids), ('wh_output_stock_loc_id', 'in', record.ids) ]) for warehouse_id in warehouses_ids: if record.branch_id and record.branch_id != warehouse_id.branch_id: raise ValidationError( _('Configuration Error of Branch:\n' 'The Location Branch (%s) and ' 'the Branch (%s) of Warehouse must ' 'be the same branch!') % (record.branch_id.name, warehouse_id.branch_id.name)) if record.usage != 'internal' and record.branch_id: raise UserError( _('Configuration error of Branch:\n' 'The branch (%s) should be assigned to internal locations' ) % (record.branch_id.name)) @api.multi @api.constrains('company_id', 'branch_id') def _check_company_branch(self): for record in self: if record.branch_id and record.company_id != record.branch_id.company_id: raise UserError( _('Configuration Error of Company:\n' 'The Company (%s) in the Stock Location and ' 'the Company (%s) of Branch must ' 'be the same company!') % (record.company_id.name, record.branch_id.company_id.name)) def name_get(self): ret_list = [] for location in self: orig_location = location name = location.name while location.location_id and location.usage != 'view': location = location.location_id if not name: raise UserError( _('You have to set a name for this location.')) name = location.name + "/" + name ret_list.append((orig_location.id, name)) return ret_list @api.model def name_search(self, name, args=None, operator='ilike', limit=100): """ search full name and barcode """ if args is None: args = [] recs = self.search([ '|', ('barcode', operator, name), ('complete_name', operator, name) ] + args, limit=limit) return recs.name_get() def get_putaway_strategy(self, product): ''' Returns the location where the product has to be put, if any compliant putaway strategy is found. Otherwise returns None.''' current_location = self putaway_location = self.env['stock.location'] while current_location and not putaway_location: if current_location.putaway_strategy_id: putaway_location = current_location.putaway_strategy_id.putaway_apply( product) current_location = current_location.location_id return putaway_location @api.returns('stock.warehouse', lambda value: value.id) def get_warehouse(self): """ Returns warehouse id of warehouse that contains location """ return self.env['stock.warehouse'].search( [('view_location_id.parent_left', '<=', self.parent_left), ('view_location_id.parent_right', '>=', self.parent_left)], limit=1) def should_bypass_reservation(self): self.ensure_one() return self.usage in ('supplier', 'customer', 'inventory', 'production') or self.scrap_location
class HrPayslip(models.Model): _name = 'hr.payslip' _description = 'Pay Slip' struct_id = fields.Many2one( 'hr.payroll.structure', string='Structure', readonly=True, states={'draft': [('readonly', False)]}, help= 'Defines the rules that have to be applied to this payslip, accordingly ' 'to the contract chosen. If you let empty the field contract, this field isn\'t ' 'mandatory anymore and thus the rules applied will be all the rules set on the ' 'structure of all contracts of the employee valid for the chosen period' ) name = fields.Char(string='Payslip Name', readonly=True, states={'draft': [('readonly', False)]}) number = fields.Char(string='Reference', readonly=True, copy=False, states={'draft': [('readonly', False)]}) employee_id = fields.Many2one('hr.employee', string='Employee', required=True, readonly=True, states={'draft': [('readonly', False)]}) date_from = fields.Date(string='Date From', readonly=True, required=True, default=time.strftime('%Y-%m-01'), states={'draft': [('readonly', False)]}) date_to = fields.Date( string='Date To', readonly=True, required=True, default=str(datetime.now() + relativedelta.relativedelta(months=+1, day=1, days=-1)) [:10], states={'draft': [('readonly', False)]}) # this is chaos: 4 states are defined, 3 are used ('verify' isn't) and 5 exist ('confirm' seems to have existed) state = fields.Selection( [ ('draft', 'Draft'), ('verify', 'Waiting'), ('done', 'Done'), ('cancel', 'Rejected'), ], string='Status', index=True, readonly=True, copy=False, default='draft', help="""* When the payslip is created the status is \'Draft\' \n* If the payslip is under verification, the status is \'Waiting\'. \n* If the payslip is confirmed then status is set to \'Done\'. \n* When user cancel payslip the status is \'Rejected\'.""") line_ids = fields.One2many('hr.payslip.line', 'slip_id', string='Payslip Lines', readonly=True, states={'draft': [('readonly', False)]}) company_id = fields.Many2one( 'res.company', string='Company', readonly=True, copy=False, default=lambda self: self.env['res.company']._company_default_get(), states={'draft': [('readonly', False)]}) worked_days_line_ids = fields.One2many( 'hr.payslip.worked_days', 'payslip_id', string='Payslip Worked Days', copy=True, readonly=True, states={'draft': [('readonly', False)]}) input_line_ids = fields.One2many('hr.payslip.input', 'payslip_id', string='Payslip Inputs', readonly=True, states={'draft': [('readonly', False)]}) paid = fields.Boolean(string='Made Payment Order ? ', readonly=True, copy=False, states={'draft': [('readonly', False)]}) note = fields.Text(string='Internal Note', readonly=True, states={'draft': [('readonly', False)]}) contract_id = fields.Many2one('hr.contract', string='Contract', readonly=True, states={'draft': [('readonly', False)]}) details_by_salary_rule_category = fields.One2many( 'hr.payslip.line', compute='_compute_details_by_salary_rule_category', string='Details by Salary Rule Category') credit_note = fields.Boolean( string='Credit Note', readonly=True, states={'draft': [('readonly', False)]}, help="Indicates this payslip has a refund of another") payslip_run_id = fields.Many2one('hr.payslip.run', string='Payslip Batches', readonly=True, copy=False, states={'draft': [('readonly', False)]}) payslip_count = fields.Integer(compute='_compute_payslip_count', string="Payslip Computation Details") @api.multi def _compute_details_by_salary_rule_category(self): for payslip in self: payslip.details_by_salary_rule_category = payslip.mapped( 'line_ids').filtered(lambda line: line.category_id) @api.multi def _compute_payslip_count(self): for payslip in self: payslip.payslip_count = len(payslip.line_ids) @api.constrains('date_from', 'date_to') def _check_dates(self): if any( self.filtered( lambda payslip: payslip.date_from > payslip.date_to)): raise ValidationError( _("Payslip 'Date From' must be before 'Date To'.")) @api.multi def action_payslip_draft(self): return self.write({'state': 'draft'}) @api.multi def action_payslip_done(self): self.compute_sheet() return self.write({'state': 'done'}) @api.multi def action_payslip_cancel(self): if self.filtered(lambda slip: slip.state == 'done'): raise UserError(_("Cannot cancel a payslip that is done.")) return self.write({'state': 'cancel'}) @api.multi def refund_sheet(self): for payslip in self: copied_payslip = payslip.copy({ 'credit_note': True, 'name': _('Refund: ') + payslip.name }) copied_payslip.compute_sheet() copied_payslip.action_payslip_done() formview_ref = self.env.ref('hr_payroll.view_hr_payslip_form', False) treeview_ref = self.env.ref('hr_payroll.view_hr_payslip_tree', False) return { 'name': ("Refund Payslip"), 'view_mode': 'tree, form', 'view_id': False, 'view_type': 'form', 'res_model': 'hr.payslip', 'type': 'ir.actions.act_window', 'target': 'current', 'domain': "[('id', 'in', %s)]" % copied_payslip.ids, 'views': [(treeview_ref and treeview_ref.id or False, 'tree'), (formview_ref and formview_ref.id or False, 'form')], 'context': {} } @api.multi def check_done(self): return True @api.multi def unlink(self): if any( self.filtered(lambda payslip: payslip.state not in ('draft', 'cancel'))): raise UserError( _('You cannot delete a payslip which is not draft or cancelled!' )) return super(HrPayslip, self).unlink() # TODO move this function into hr_contract module, on hr.employee object @api.model def get_contract(self, employee, date_from, date_to): """ @param employee: recordset of employee @param date_from: date field @param date_to: date field @return: returns the ids of all the contracts for the given employee that need to be considered for the given dates """ # a contract is valid if it ends between the given dates clause_1 = [ '&', ('date_end', '<=', date_to), ('date_end', '>=', date_from) ] # OR if it starts between the given dates clause_2 = [ '&', ('date_start', '<=', date_to), ('date_start', '>=', date_from) ] # OR if it starts before the date_from and finish after the date_end (or never finish) clause_3 = [ '&', ('date_start', '<=', date_from), '|', ('date_end', '=', False), ('date_end', '>=', date_to) ] clause_final = [('employee_id', '=', employee.id), ('state', '=', 'open'), '|', '|' ] + clause_1 + clause_2 + clause_3 return self.env['hr.contract'].search(clause_final).ids @api.multi def compute_sheet(self): for payslip in self: number = payslip.number or self.env['ir.sequence'].next_by_code( 'salary.slip') # delete old payslip lines payslip.line_ids.unlink() # set the list of contract for which the rules have to be applied # if we don't give the contract, then the rules to apply should be for all current contracts of the employee contract_ids = payslip.contract_id.ids or \ self.get_contract(payslip.employee_id, payslip.date_from, payslip.date_to) lines = [ (0, 0, line) for line in self._get_payslip_lines(contract_ids, payslip.id) ] payslip.write({'line_ids': lines, 'number': number}) return True @api.model def get_worked_day_lines(self, contracts, date_from, date_to): """ @param contract: Browse record of contracts @return: returns a list of dict containing the input that should be applied for the given contract between date_from and date_to """ res = [] # fill only if the contract as a working schedule linked for contract in contracts.filtered( lambda contract: contract.resource_calendar_id): day_from = datetime.combine(fields.Date.from_string(date_from), datetime_time.min) day_to = datetime.combine(fields.Date.from_string(date_to), datetime_time.max) # compute leave days leaves = {} day_leave_intervals = contract.employee_id.iter_leaves( day_from, day_to, calendar=contract.resource_calendar_id) for day_intervals in day_leave_intervals: for interval in day_intervals: holiday = interval[2]['leaves'].holiday_id current_leave_struct = leaves.setdefault( holiday.holiday_status_id, { 'name': holiday.holiday_status_id.name, 'sequence': 5, 'code': holiday.holiday_status_id.name, 'number_of_days': 0.0, 'number_of_hours': 0.0, 'contract_id': contract.id, }) leave_time = (interval[1] - interval[0]).seconds / 3600 current_leave_struct['number_of_hours'] += leave_time work_hours = contract.employee_id.get_day_work_hours_count( interval[0].date(), calendar=contract.resource_calendar_id) if work_hours: current_leave_struct[ 'number_of_days'] += leave_time / work_hours # compute worked days work_data = contract.employee_id.get_work_days_data( day_from, day_to, calendar=contract.resource_calendar_id) attendances = { 'name': _("Normal Working Days paid at 100%"), 'sequence': 1, 'code': 'WORK100', 'number_of_days': work_data['days'], 'number_of_hours': work_data['hours'], 'contract_id': contract.id, } res.append(attendances) res.extend(leaves.values()) return res @api.model def get_inputs(self, contracts, date_from, date_to): res = [] structure_ids = contracts.get_all_structures() rule_ids = self.env['hr.payroll.structure'].browse( structure_ids).get_all_rules() sorted_rule_ids = [ id for id, sequence in sorted(rule_ids, key=lambda x: x[1]) ] inputs = self.env['hr.salary.rule'].browse(sorted_rule_ids).mapped( 'input_ids') for contract in contracts: for input in inputs: input_data = { 'name': input.name, 'code': input.code, 'contract_id': contract.id, } res += [input_data] return res @api.model def _get_payslip_lines(self, contract_ids, payslip_id): def _sum_salary_rule_category(localdict, category, amount): if category.parent_id: localdict = _sum_salary_rule_category(localdict, category.parent_id, amount) localdict['categories'].dict[ category.code] = category.code in localdict[ 'categories'].dict and localdict['categories'].dict[ category.code] + amount or amount return localdict class BrowsableObject(object): def __init__(self, employee_id, dict, env): self.employee_id = employee_id self.dict = dict self.env = env def __getattr__(self, attr): return attr in self.dict and self.dict.__getitem__(attr) or 0.0 class InputLine(BrowsableObject): """a class that will be used into the python code, mainly for usability purposes""" def sum(self, code, from_date, to_date=None): if to_date is None: to_date = fields.Date.today() self.env.cr.execute( """ SELECT sum(amount) as sum FROM hr_payslip as hp, hr_payslip_input as pi WHERE hp.employee_id = %s AND hp.state = 'done' AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s""", (self.employee_id, from_date, to_date, code)) return self.env.cr.fetchone()[0] or 0.0 class WorkedDays(BrowsableObject): """a class that will be used into the python code, mainly for usability purposes""" def _sum(self, code, from_date, to_date=None): if to_date is None: to_date = fields.Date.today() self.env.cr.execute( """ SELECT sum(number_of_days) as number_of_days, sum(number_of_hours) as number_of_hours FROM hr_payslip as hp, hr_payslip_worked_days as pi WHERE hp.employee_id = %s AND hp.state = 'done' AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pi.payslip_id AND pi.code = %s""", (self.employee_id, from_date, to_date, code)) return self.env.cr.fetchone() def sum(self, code, from_date, to_date=None): res = self._sum(code, from_date, to_date) return res and res[0] or 0.0 def sum_hours(self, code, from_date, to_date=None): res = self._sum(code, from_date, to_date) return res and res[1] or 0.0 class Payslips(BrowsableObject): """a class that will be used into the python code, mainly for usability purposes""" def sum(self, code, from_date, to_date=None): if to_date is None: to_date = fields.Date.today() self.env.cr.execute( """SELECT sum(case when hp.credit_note = False then (pl.total) else (-pl.total) end) FROM hr_payslip as hp, hr_payslip_line as pl WHERE hp.employee_id = %s AND hp.state = 'done' AND hp.date_from >= %s AND hp.date_to <= %s AND hp.id = pl.slip_id AND pl.code = %s""", (self.employee_id, from_date, to_date, code)) res = self.env.cr.fetchone() return res and res[0] or 0.0 #we keep a dict with the result because a value can be overwritten by another rule with the same code result_dict = {} rules_dict = {} worked_days_dict = {} inputs_dict = {} blacklist = [] payslip = self.env['hr.payslip'].browse(payslip_id) for worked_days_line in payslip.worked_days_line_ids: worked_days_dict[worked_days_line.code] = worked_days_line for input_line in payslip.input_line_ids: inputs_dict[input_line.code] = input_line categories = BrowsableObject(payslip.employee_id.id, {}, self.env) inputs = InputLine(payslip.employee_id.id, inputs_dict, self.env) worked_days = WorkedDays(payslip.employee_id.id, worked_days_dict, self.env) payslips = Payslips(payslip.employee_id.id, payslip, self.env) rules = BrowsableObject(payslip.employee_id.id, rules_dict, self.env) baselocaldict = { 'categories': categories, 'rules': rules, 'payslip': payslips, 'worked_days': worked_days, 'inputs': inputs } #get the ids of the structures on the contracts and their parent id as well contracts = self.env['hr.contract'].browse(contract_ids) if len(contracts) == 1 and payslip.struct_id: structure_ids = list( set(payslip.struct_id._get_parent_structure().ids)) else: structure_ids = contracts.get_all_structures() #get the rules of the structure and thier children rule_ids = self.env['hr.payroll.structure'].browse( structure_ids).get_all_rules() #run the rules by sequence sorted_rule_ids = [ id for id, sequence in sorted(rule_ids, key=lambda x: x[1]) ] sorted_rules = self.env['hr.salary.rule'].browse(sorted_rule_ids) for contract in contracts: employee = contract.employee_id localdict = dict(baselocaldict, employee=employee, contract=contract) for rule in sorted_rules: key = rule.code + '-' + str(contract.id) localdict['result'] = None localdict['result_qty'] = 1.0 localdict['result_rate'] = 100 #check if the rule can be applied if rule._satisfy_condition( localdict) and rule.id not in blacklist: #compute the amount of the rule amount, qty, rate = rule._compute_rule(localdict) #check if there is already a rule computed with that code previous_amount = rule.code in localdict and localdict[ rule.code] or 0.0 #set/overwrite the amount computed for this rule in the localdict tot_rule = amount * qty * rate / 100.0 localdict[rule.code] = tot_rule rules_dict[rule.code] = rule #sum the amount for its salary category localdict = _sum_salary_rule_category( localdict, rule.category_id, tot_rule - previous_amount) #create/overwrite the rule in the temporary results result_dict[key] = { 'salary_rule_id': rule.id, 'contract_id': contract.id, 'name': rule.name, 'code': rule.code, 'category_id': rule.category_id.id, 'sequence': rule.sequence, 'appears_on_payslip': rule.appears_on_payslip, 'condition_select': rule.condition_select, 'condition_python': rule.condition_python, 'condition_range': rule.condition_range, 'condition_range_min': rule.condition_range_min, 'condition_range_max': rule.condition_range_max, 'amount_select': rule.amount_select, 'amount_fix': rule.amount_fix, 'amount_python_compute': rule.amount_python_compute, 'amount_percentage': rule.amount_percentage, 'amount_percentage_base': rule.amount_percentage_base, 'register_id': rule.register_id.id, 'amount': amount, 'employee_id': contract.employee_id.id, 'quantity': qty, 'rate': rate, } else: #blacklist this rule and its children blacklist += [ id for id, seq in rule._recursive_search_of_rules() ] return list(result_dict.values()) # YTI TODO To rename. This method is not really an onchange, as it is not in any view # employee_id and contract_id could be browse records @api.multi def onchange_employee_id(self, date_from, date_to, employee_id=False, contract_id=False): #defaults res = { 'value': { 'line_ids': [], #delete old input lines 'input_line_ids': [( 2, x, ) for x in self.input_line_ids.ids], #delete old worked days lines 'worked_days_line_ids': [( 2, x, ) for x in self.worked_days_line_ids.ids], #'details_by_salary_head':[], TODO put me back 'name': '', 'contract_id': False, 'struct_id': False, } } if (not employee_id) or (not date_from) or (not date_to): return res ttyme = datetime.fromtimestamp( time.mktime(time.strptime(date_from, "%Y-%m-%d"))) employee = self.env['hr.employee'].browse(employee_id) locale = self.env.context.get('lang') or 'en_US' res['value'].update({ 'name': _('Salary Slip of %s for %s') % (employee.name, tools.ustr( babel.dates.format_date( date=ttyme, format='MMMM-y', locale=locale))), 'company_id': employee.company_id.id, }) if not self.env.context.get('contract'): #fill with the first contract of the employee contract_ids = self.get_contract(employee, date_from, date_to) else: if contract_id: #set the list of contract for which the input have to be filled contract_ids = [contract_id] else: #if we don't give the contract, then the input to fill should be for all current contracts of the employee contract_ids = self.get_contract(employee, date_from, date_to) if not contract_ids: return res contract = self.env['hr.contract'].browse(contract_ids[0]) res['value'].update({'contract_id': contract.id}) struct = contract.struct_id if not struct: return res res['value'].update({ 'struct_id': struct.id, }) #computation of the salary input contracts = self.env['hr.contract'].browse(contract_ids) worked_days_line_ids = self.get_worked_day_lines( contracts, date_from, date_to) input_line_ids = self.get_inputs(contracts, date_from, date_to) res['value'].update({ 'worked_days_line_ids': worked_days_line_ids, 'input_line_ids': input_line_ids, }) return res @api.onchange('employee_id', 'date_from', 'date_to') def onchange_employee(self): if (not self.employee_id) or (not self.date_from) or ( not self.date_to): return employee = self.employee_id date_from = self.date_from date_to = self.date_to contract_ids = [] ttyme = datetime.fromtimestamp( time.mktime(time.strptime(date_from, "%Y-%m-%d"))) locale = self.env.context.get('lang') or 'en_US' self.name = _('Salary Slip of %s for %s') % ( employee.name, tools.ustr( babel.dates.format_date( date=ttyme, format='MMMM-y', locale=locale))) self.company_id = employee.company_id if not self.env.context.get('contract') or not self.contract_id: contract_ids = self.get_contract(employee, date_from, date_to) if not contract_ids: return self.contract_id = self.env['hr.contract'].browse(contract_ids[0]) if not self.contract_id.struct_id: return self.struct_id = self.contract_id.struct_id #computation of the salary input contracts = self.env['hr.contract'].browse(contract_ids) worked_days_line_ids = self.get_worked_day_lines( contracts, date_from, date_to) worked_days_lines = self.worked_days_line_ids.browse([]) for r in worked_days_line_ids: worked_days_lines += worked_days_lines.new(r) self.worked_days_line_ids = worked_days_lines input_line_ids = self.get_inputs(contracts, date_from, date_to) input_lines = self.input_line_ids.browse([]) for r in input_line_ids: input_lines += input_lines.new(r) self.input_line_ids = input_lines return @api.onchange('contract_id') def onchange_contract(self): if not self.contract_id: self.struct_id = False self.with_context(contract=True).onchange_employee() return def get_salary_line_total(self, code): self.ensure_one() line = self.line_ids.filtered(lambda line: line.code == code) if line: return line[0].total else: return 0.0
class Route(models.Model): _name = 'stock.location.route' _description = "Inventory Routes" _order = 'sequence' name = fields.Char('Route Name', required=True, translate=True) active = fields.Boolean( 'Active', default=True, help= "If the active field is set to False, it will allow you to hide the route without removing it." ) sequence = fields.Integer('Sequence', default=0) pull_ids = fields.One2many('procurement.rule', 'route_id', 'Procurement Rules', copy=True, help="The demand represented by a procurement from e.g. a sale order, a reordering rule, another move, needs to be solved by applying a procurement rule. Depending on the action on the procurement rule,"\ "this triggers a purchase order, manufacturing order or another move. This way we create chains in the reverse order from the endpoint with the original demand to the starting point. "\ "That way, it is always known where we need to go and that is why they are preferred over push rules.") push_ids = fields.One2many( 'stock.location.path', 'route_id', 'Push Rules', copy=True, help= "When a move is foreseen to a location, the push rule will automatically create a move to a next location after. This is mainly only needed when creating manual operations e.g. 2/3 step manual purchase order or 2/3 step finished product manual manufacturing order. In other cases, it is important to use pull rules where you know where you are going based on a demand." ) product_selectable = fields.Boolean( 'Applicable on Product', default=True, help= "When checked, the route will be selectable in the Inventory tab of the Product form. It will take priority over the Warehouse route. " ) product_categ_selectable = fields.Boolean( 'Applicable on Product Category', help= "When checked, the route will be selectable on the Product Category. It will take priority over the Warehouse route. " ) warehouse_selectable = fields.Boolean( 'Applicable on Warehouse', help= "When a warehouse is selected for this route, this route should be seen as the default route when products pass through this warehouse. This behaviour can be overridden by the routes on the Product/Product Categories or by the Preferred Routes on the Procurement" ) supplied_wh_id = fields.Many2one('stock.warehouse', 'Supplied Warehouse') supplier_wh_id = fields.Many2one('stock.warehouse', 'Supplying Warehouse') company_id = fields.Many2one( 'res.company', 'Company', default=lambda self: self.env['res.company']._company_default_get( 'stock.location.route'), index=True, help= 'Leave this field empty if this route is shared between all companies') product_ids = fields.Many2many('product.template', 'stock_route_product', 'route_id', 'product_id', 'Products') categ_ids = fields.Many2many('product.category', 'stock_location_route_categ', 'route_id', 'categ_id', 'Product Categories') warehouse_ids = fields.Many2many('stock.warehouse', 'stock_route_warehouse', 'route_id', 'warehouse_id', 'Warehouses') def write(self, values): '''when a route is deactivated, deactivate also its pull and push rules''' res = super(Route, self).write(values) if 'active' in values: self.mapped('push_ids').filtered( lambda path: path.active != values['active']).write( {'active': values['active']}) self.mapped('pull_ids').filtered( lambda rule: rule.active != values['active']).write( {'active': values['active']}) return res def view_product_ids(self): return { 'name': _('Products'), 'view_type': 'form', 'view_mode': 'tree,form', 'res_model': 'product.template', 'type': 'ir.actions.act_window', 'domain': [('route_ids', 'in', self.ids)], } def view_categ_ids(self): return { 'name': _('Product Categories'), 'view_type': 'form', 'view_mode': 'tree,form', 'res_model': 'product.category', 'type': 'ir.actions.act_window', 'domain': [('route_ids', 'in', self.ids)], }
class Users(models.Model): """ User class. A res.users record models an OpenERP user and is different from an employee. res.users class now inherits from res.partner. The partner model is used to store the data related to the partner: lang, name, address, avatar, ... The user model is now dedicated to technical data. """ _name = "res.users" _description = 'Users' _inherits = {'res.partner': 'partner_id'} _order = 'name, login' __uid_cache = defaultdict(dict) # {dbname: {uid: password}} # User can write on a few of his own fields (but not his groups for example) SELF_WRITEABLE_FIELDS = ['signature', 'action_id', 'company_id', 'email', 'name', 'image', 'image_medium', 'image_small', 'lang', 'tz'] # User can read a few of his own fields SELF_READABLE_FIELDS = ['signature', 'company_id', 'login', 'email', 'name', 'image', 'image_medium', 'image_small', 'lang', 'tz', 'tz_offset', 'groups_id', 'partner_id', '__last_update', 'action_id'] def _default_groups(self): default_user = self.env.ref('base.default_user', raise_if_not_found=False) return (default_user or self.env['res.users']).sudo().groups_id def _companies_count(self): return self.env['res.company'].sudo().search_count([]) partner_id = fields.Many2one('res.partner', required=True, ondelete='restrict', auto_join=True, string='Related Partner', help='Partner-related data of the user') login = fields.Char(required=True, help="Used to log into the system") password = fields.Char(default='', invisible=True, copy=False, help="Keep empty if you don't want the user to be able to connect on the system.") new_password = fields.Char(string='Set Password', compute='_compute_password', inverse='_inverse_password', help="Specify a value only when creating a user or if you're "\ "changing the user's password, otherwise leave empty. After "\ "a change of password, the user has to login again.") signature = fields.Html() active = fields.Boolean(default=True) action_id = fields.Many2one('ir.actions.actions', string='Home Action', help="If specified, this action will be opened at log on for this user, in addition to the standard menu.") groups_id = fields.Many2many('res.groups', 'res_groups_users_rel', 'uid', 'gid', string='Groups', default=_default_groups) log_ids = fields.One2many('res.users.log', 'create_uid', string='User log entries') login_date = fields.Datetime(related='log_ids.create_date', string='Latest connection') share = fields.Boolean(compute='_compute_share', compute_sudo=True, string='Share User', store=True, help="External user with limited access, created only for the purpose of sharing data.") companies_count = fields.Integer(compute='_compute_companies_count', string="Number of Companies", default=_companies_count) tz_offset = fields.Char(compute='_compute_tz_offset', string='Timezone offset', invisible=True) @api.model def _get_company(self): return self.env.user.company_id # Special behavior for this field: res.company.search() will only return the companies # available to the current user (should be the user's companies?), when the user_preference # context is set. company_id = fields.Many2one('res.company', string='Company', required=True, default=_get_company, help='The company this user is currently working for.', context={'user_preference': True}) company_ids = fields.Many2many('res.company', 'res_company_users_rel', 'user_id', 'cid', string='Companies', default=_get_company) # overridden inherited fields to bypass access rights, in case you have # access to the user but not its corresponding partner name = fields.Char(related='partner_id.name', inherited=True) email = fields.Char(related='partner_id.email', inherited=True) _sql_constraints = [ ('login_key', 'UNIQUE (login)', 'You can not have two users with the same login !') ] def _compute_password(self): for user in self: user.password = '' def _inverse_password(self): for user in self: if not user.new_password: # Do not update the password if no value is provided, ignore silently. # For example web client submits False values for all empty fields. continue if user == self.env.user: # To change their own password, users must use the client-specific change password wizard, # so that the new password is immediately used for further RPC requests, otherwise the user # will face unexpected 'Access Denied' exceptions. raise UserError(_('Please use the change password wizard (in User Preferences or User menu) to change your own password.')) else: user.password = user.new_password @api.depends('groups_id') def _compute_share(self): for user in self: user.share = not user.has_group('base.group_user') @api.multi def _compute_companies_count(self): companies_count = self._companies_count() for user in self: user.companies_count = companies_count @api.depends('tz') def _compute_tz_offset(self): for user in self: user.tz_offset = datetime.datetime.now(pytz.timezone(user.tz or 'GMT')).strftime('%z') @api.onchange('login') def on_change_login(self): if self.login and tools.single_email_re.match(self.login): self.email = self.login @api.onchange('parent_id') def onchange_parent_id(self): return self.mapped('partner_id').onchange_parent_id() @api.multi @api.constrains('company_id', 'company_ids') def _check_company(self): if any(user.company_ids and user.company_id not in user.company_ids for user in self): raise ValidationError(_('The chosen company is not in the allowed companies for this user')) @api.multi @api.constrains('action_id') def _check_action_id(self): action_open_website = self.env.ref('base.action_open_website', raise_if_not_found=False) if action_open_website and any(user.action_id.id == action_open_website.id for user in self): raise ValidationError(_('The "App Switcher" action cannot be selected as home action.')) @api.multi def read(self, fields=None, load='_classic_read'): if fields and self == self.env.user: for key in fields: if not (key in self.SELF_READABLE_FIELDS or key.startswith('context_')): break else: # safe fields only, so we read as super-user to bypass access rights self = self.sudo() result = super(Users, self).read(fields=fields, load=load) canwrite = self.env['ir.model.access'].check('res.users', 'write', False) if not canwrite: for vals in result: if vals['id'] != self._uid: for key in USER_PRIVATE_FIELDS: if key in vals: vals[key] = '********' return result @api.model def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True): groupby_fields = set([groupby] if isinstance(groupby, pycompat.string_types) else groupby) if groupby_fields.intersection(USER_PRIVATE_FIELDS): raise AccessError(_("Invalid 'group by' parameter")) return super(Users, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy) @api.model def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None): if self._uid != SUPERUSER_ID and args: domain_fields = {term[0] for term in args if isinstance(term, (tuple, list))} if domain_fields.intersection(USER_PRIVATE_FIELDS): raise AccessError(_('Invalid search criterion')) return super(Users, self)._search(args, offset=offset, limit=limit, order=order, count=count, access_rights_uid=access_rights_uid) @api.model def create(self, vals): user = super(Users, self).create(vals) user.partner_id.active = user.active if user.partner_id.company_id: user.partner_id.write({'company_id': user.company_id.id}) return user @api.multi def write(self, values): if values.get('active') == False: for user in self: if user.id == SUPERUSER_ID: raise UserError(_("You cannot deactivate the admin user.")) elif user.id == self._uid: raise UserError(_("You cannot deactivate the user you're currently logged in as.")) if self == self.env.user: for key in list(values): if not (key in self.SELF_WRITEABLE_FIELDS or key.startswith('context_')): break else: if 'company_id' in values: if values['company_id'] not in self.env.user.company_ids.ids: del values['company_id'] # safe fields only, so we write as super-user to bypass access rights self = self.sudo() res = super(Users, self).write(values) if 'company_id' in values: for user in self: # if partner is global we keep it that way if user.partner_id.company_id and user.partner_id.company_id.id != values['company_id']: user.partner_id.write({'company_id': user.company_id.id}) # clear default ir values when company changes self.env['ir.default'].clear_caches() # clear caches linked to the users if 'groups_id' in values: self.env['ir.model.access'].call_cache_clearing_methods() self.env['ir.rule'].clear_caches() self.has_group.clear_cache(self) if any(key.startswith('context_') or key in ('lang', 'tz') for key in values): self.context_get.clear_cache(self) if any(key in values for key in ['active'] + USER_PRIVATE_FIELDS): db = self._cr.dbname for id in self.ids: self.__uid_cache[db].pop(id, None) if any(key in values for key in self._get_session_token_fields()): self._invalidate_session_cache() return res @api.multi def unlink(self): if SUPERUSER_ID in self.ids: raise UserError(_('You can not remove the admin user as it is used internally for resources created by actpy (updates, module installation, ...)')) db = self._cr.dbname for id in self.ids: self.__uid_cache[db].pop(id, None) self._invalidate_session_cache() return super(Users, self).unlink() @api.model def name_search(self, name='', args=None, operator='ilike', limit=100): if args is None: args = [] users = self.browse() if name and operator in ['=', 'ilike']: users = self.search([('login', '=', name)] + args, limit=limit) if not users: users = self.search([('name', operator, name)] + args, limit=limit) return users.name_get() @api.multi def copy(self, default=None): self.ensure_one() default = dict(default or {}) if ('name' not in default) and ('partner_id' not in default): default['name'] = _("%s (copy)") % self.name if 'login' not in default: default['login'] = _("%s (copy)") % self.login return super(Users, self).copy(default) @api.model @tools.ormcache('self._uid') def context_get(self): user = self.env.user result = {} for k in self._fields: if k.startswith('context_'): context_key = k[8:] elif k in ['lang', 'tz']: context_key = k else: context_key = False if context_key: res = getattr(user, k) or False if isinstance(res, models.BaseModel): res = res.id result[context_key] = res or False return result @api.model @api.returns('ir.actions.act_window', lambda record: record.id) def action_get(self): return self.sudo().env.ref('base.action_res_users_my') def check_super(self, passwd): return check_super(passwd) @api.model def check_credentials(self, password): """ Override this method to plug additional authentication methods""" user = self.sudo().search([('id', '=', self._uid), ('password', '=', password)]) if not user: raise AccessDenied() @api.model def _update_last_login(self): # only create new records to avoid any side-effect on concurrent transactions # extra records will be deleted by the periodical garbage collection self.env['res.users.log'].create({}) # populated by defaults @classmethod def _login(cls, db, login, password): if not password: return False user_id = False try: with cls.pool.cursor() as cr: self = api.Environment(cr, SUPERUSER_ID, {})[cls._name] user = self.search([('login', '=', login)]) if user: user_id = user.id user.sudo(user_id).check_credentials(password) user.sudo(user_id)._update_last_login() except AccessDenied: _logger.info("Login failed for db:%s login:%s", db, login) user_id = False return user_id @classmethod def authenticate(cls, db, login, password, user_agent_env): """Verifies and returns the user ID corresponding to the given ``login`` and ``password`` combination, or False if there was no matching user. :param str db: the database on which user is trying to authenticate :param str login: username :param str password: user password :param dict user_agent_env: environment dictionary describing any relevant environment attributes """ uid = cls._login(db, login, password) if uid == SUPERUSER_ID: # Successfully logged in as admin! # Attempt to guess the web base url... if user_agent_env and user_agent_env.get('base_location'): try: with cls.pool.cursor() as cr: base = user_agent_env['base_location'] ICP = api.Environment(cr, uid, {})['ir.config_parameter'] if not ICP.get_param('web.base.url.freeze'): ICP.set_param('web.base.url', base) except Exception: _logger.exception("Failed to update web.base.url configuration parameter") return uid @classmethod def check(cls, db, uid, passwd): """Verifies that the given (uid, password) is authorized for the database ``db`` and raise an exception if it is not.""" if not passwd: # empty passwords disallowed for obvious security reasons raise AccessDenied() db = cls.pool.db_name if cls.__uid_cache[db].get(uid) == passwd: return cr = cls.pool.cursor() try: self = api.Environment(cr, uid, {})[cls._name] self.check_credentials(passwd) cls.__uid_cache[db][uid] = passwd finally: cr.close() def _get_session_token_fields(self): return {'id', 'login', 'password', 'active'} @tools.ormcache('sid') def _compute_session_token(self, sid): """ Compute a session token given a session id and a user id """ # retrieve the fields used to generate the session token session_fields = ', '.join(sorted(self._get_session_token_fields())) self.env.cr.execute("""SELECT %s, (SELECT value FROM ir_config_parameter WHERE key='database.secret') FROM res_users WHERE id=%%s""" % (session_fields), (self.id,)) if self.env.cr.rowcount != 1: self._invalidate_session_cache() return False data_fields = self.env.cr.fetchone() # generate hmac key key = (u'%s' % (data_fields,)).encode('utf-8') # hmac the session id data = sid.encode('utf-8') h = hmac.new(key, data, sha256) # keep in the cache the token return h.hexdigest() @api.multi def _invalidate_session_cache(self): """ Clear the sessions cache """ self._compute_session_token.clear_cache(self) @api.model def change_password(self, old_passwd, new_passwd): """Change current user password. Old password must be provided explicitly to prevent hijacking an existing user session, or for cases where the cleartext password is not used to authenticate requests. :return: True :raise: actpy.exceptions.AccessDenied when old password is wrong :raise: actpy.exceptions.UserError when new password is not set or empty """ self.check(self._cr.dbname, self._uid, old_passwd) if new_passwd: # use self.env.user here, because it has uid=SUPERUSER_ID return self.env.user.write({'password': new_passwd}) raise UserError(_("Setting empty passwords is not allowed for security reasons!")) @api.multi def preference_save(self): return { 'type': 'ir.actions.client', 'tag': 'reload_context', } @api.multi def preference_change_password(self): return { 'type': 'ir.actions.client', 'tag': 'change_password', 'target': 'new', } @api.model def has_group(self, group_ext_id): # use singleton's id if called on a non-empty recordset, otherwise # context uid uid = self.id or self._uid return self.sudo(user=uid)._has_group(group_ext_id) @api.model @tools.ormcache('self._uid', 'group_ext_id') def _has_group(self, group_ext_id): """Checks whether user belongs to given group. :param str group_ext_id: external ID (XML ID) of the group. Must be provided in fully-qualified form (``module.ext_id``), as there is no implicit module to use.. :return: True if the current user is a member of the group with the given external ID (XML ID), else False. """ assert group_ext_id and '.' in group_ext_id, "External ID must be fully qualified" module, ext_id = group_ext_id.split('.') self._cr.execute("""SELECT 1 FROM res_groups_users_rel WHERE uid=%s AND gid IN (SELECT res_id FROM ir_model_data WHERE module=%s AND name=%s)""", (self._uid, module, ext_id)) return bool(self._cr.fetchone()) # for a few places explicitly clearing the has_group cache has_group.clear_cache = _has_group.clear_cache @api.multi def _is_public(self): self.ensure_one() return self.has_group('base.group_public') @api.multi def _is_system(self): self.ensure_one() return self.has_group('base.group_system') @api.multi def _is_admin(self): self.ensure_one() return self._is_superuser() or self.has_group('base.group_erp_manager') @api.multi def _is_superuser(self): self.ensure_one() return self.id == SUPERUSER_ID @api.model def get_company_currency_id(self): return self.env.user.company_id.currency_id.id
class RatingMixin(models.AbstractModel): _name = 'rating.mixin' _description = "Rating Mixin" rating_ids = fields.One2many( 'rating.rating', 'res_id', string='Rating', domain=lambda self: [('res_model', '=', self._name)], auto_join=True) rating_last_value = fields.Float('Rating Last Value', compute='_compute_rating_last_value', compute_sudo=True, store=True) rating_last_feedback = fields.Text('Rating Last Feedback', related='rating_ids.feedback') rating_last_image = fields.Binary('Rating Last Image', related='rating_ids.rating_image') rating_count = fields.Integer('Rating count', compute="_compute_rating_count") @api.multi @api.depends('rating_ids.rating') def _compute_rating_last_value(self): for record in self: ratings = self.env['rating.rating'].search( [('res_model', '=', self._name), ('res_id', '=', record.id)], limit=1) if ratings: record.rating_last_value = ratings.rating @api.multi def _compute_rating_count(self): read_group_res = self.env['rating.rating'].read_group( [('res_model', '=', self._name), ('res_id', 'in', self.ids), ('consumed', '=', True)], ['res_id'], groupby=['res_id']) result = dict.fromkeys(self.ids, 0) for data in read_group_res: result[data['res_id']] += data['res_id_count'] for record in self: record.rating_count = result.get(record.id) def write(self, values): """ If the rated ressource name is modified, we should update the rating res_name too. """ result = super(RatingMixin, self).write(values) if self._rec_name in values: self.rating_ids._compute_res_name() return result def unlink(self): """ When removing a record, its rating should be deleted too. """ record_ids = self.ids result = super(RatingMixin, self).unlink() self.env['rating.rating'].sudo().search([ ('res_model', '=', self._name), ('res_id', 'in', record_ids) ]).unlink() return result def rating_get_parent_model_name(self, vals): """ Return the parent model name """ return None def rating_get_parent_id(self): """ Return the parent record id """ return None def rating_get_partner_id(self): if hasattr(self, 'partner_id') and self.partner_id: return self.partner_id return self.env['res.partner'] def rating_get_rated_partner_id(self): if hasattr(self, 'user_id') and self.user_id.partner_id: return self.user_id.partner_id return self.env['res.partner'] def rating_get_access_token(self, partner=None): if not partner: partner = self.rating_get_partner_id() rated_partner = self.rating_get_rated_partner_id() ratings = self.rating_ids.filtered( lambda x: x.partner_id.id == partner.id and not x.consumed) if not ratings: record_model_id = self.env['ir.model'].sudo().search( [('model', '=', self._name)], limit=1).id rating = self.env['rating.rating'].create({ 'partner_id': partner.id, 'rated_partner_id': rated_partner.id, 'res_model_id': record_model_id, 'res_id': self.id }) else: rating = ratings[0] return rating.access_token @api.multi def rating_send_request(self, template, lang=False, force_send=True): """ This method send rating request by email, using a template given in parameter. """ lang = lang or 'en_US' for record in self: template.with_context(lang=lang).send_mail(record.id, force_send=force_send) @api.multi def rating_apply(self, rate, token=None, feedback=None, subtype=None): """ Apply a rating given a token. If the current model inherits from mail.thread mixing, a message is posted on its chatter. :param rate : the rating value to apply :type rate : float :param token : access token :param feedback : additional feedback :type feedback : string :param subtype : subtype for mail :type subtype : string :returns rating.rating record """ Rating, rating = self.env['rating.rating'], None if token: rating = self.env['rating.rating'].search( [('access_token', '=', token)], limit=1) else: rating = Rating.search([('res_model', '=', self._name), ('res_id', '=', self.ids[0])], limit=1) if rating: rating.write({ 'rating': rate, 'feedback': feedback, 'consumed': True }) if hasattr(self, 'message_post'): feedback = tools.plaintext2html(feedback or '') self.message_post( body= "<img src='/rating/static/src/img/rating_%s.png' alt=':rating_%s' style='width:20px;height:20px;float:left;margin-right: 5px;'/>%s" % (rate, rate, feedback), subtype=subtype or "mail.mt_comment", author_id=rating.partner_id and rating.partner_id.id or None # None will set the default author in mail_thread.py ) if hasattr(self, 'stage_id') and self.stage_id and hasattr( self.stage_id, 'auto_validation_kanban_state' ) and self.stage_id.auto_validation_kanban_state: if rating.rating > 5: self.write({'kanban_state': 'done'}) if rating.rating < 5: self.write({'kanban_state': 'blocked'}) return rating @api.multi def rating_get_repartition(self, add_stats=False, domain=None): """ get the repatition of rating grade for the given res_ids. :param add_stats : flag to add stat to the result :type add_stats : boolean :param domain : optional extra domain of the rating to include/exclude in repartition :return dictionnary if not add_stats, the dict is like - key is the rating value (integer) - value is the number of object (res_model, res_id) having the value otherwise, key is the value of the information (string) : either stat name (avg, total, ...) or 'repartition' containing the same dict if add_stats was False. """ base_domain = [('res_model', '=', self._name), ('res_id', 'in', self.ids), ('rating', '>=', 1), ('consumed', '=', True)] if domain: base_domain += domain data = self.env['rating.rating'].read_group(base_domain, ['rating'], ['rating', 'res_id']) # init dict with all posible rate value, except 0 (no value for the rating) values = dict.fromkeys(range(1, 11), 0) values.update((d['rating'], d['rating_count']) for d in data) # add other stats if add_stats: rating_number = sum(values.values()) result = { 'repartition': values, 'avg': sum(float(key * values[key]) for key in values) / rating_number if rating_number > 0 else 0, 'total': sum(it['rating_count'] for it in data), } return result return values @api.multi def rating_get_grades(self, domain=None): """ get the repatition of rating grade for the given res_ids. :param domain : optional domain of the rating to include/exclude in grades computation :return dictionnary where the key is the grade (great, okay, bad), and the value, the number of object (res_model, res_id) having the grade the grade are compute as 0-30% : Bad 31-69%: Okay 70-100%: Great """ data = self.rating_get_repartition(domain=domain) res = dict.fromkeys(['great', 'okay', 'bad'], 0) for key in data: if key >= RATING_LIMIT_SATISFIED: res['great'] += data[key] elif key > RATING_LIMIT_OK: res['okay'] += data[key] else: res['bad'] += data[key] return res @api.multi def rating_get_stats(self, domain=None): """ get the statistics of the rating repatition :param domain : optional domain of the rating to include/exclude in statistic computation :return dictionnary where - key is the the name of the information (stat name) - value is statistic value : 'percent' contains the repartition in percentage, 'avg' is the average rate and 'total' is the number of rating """ data = self.rating_get_repartition(domain=domain, add_stats=True) result = { 'avg': data['avg'], 'total': data['total'], 'percent': dict.fromkeys(range(1, 11), 0), } for rate in data['repartition']: result['percent'][rate] = ( data['repartition'][rate] * 100) / data['total'] if data['total'] > 0 else 0 return result
class PurchaseRequisition(models.Model): _name = "purchase.requisition" _description = "Purchase Requisition" _inherit = ['mail.thread'] _order = "id desc" def _get_picking_in(self): pick_in = self.env.ref('stock.picking_type_in') if not pick_in: company = self.env['res.company']._company_default_get( 'purchase.requisition') pick_in = self.env['stock.picking.type'].search( [('warehouse_id.company_id', '=', company.id), ('code', '=', 'incoming')], limit=1, ) return pick_in def _get_type_id(self): return self.env['purchase.requisition.type'].search([], limit=1) name = fields.Char(string='Agreement Reference', required=True, copy=False, default=lambda self: self.env['ir.sequence']. next_by_code('purchase.order.requisition')) origin = fields.Char(string='Source Document') order_count = fields.Integer(compute='_compute_orders_number', string='Number of Orders') vendor_id = fields.Many2one('res.partner', string="Vendor") type_id = fields.Many2one('purchase.requisition.type', string="Agreement Type", required=True, default=_get_type_id) ordering_date = fields.Date(string="Ordering Date") date_end = fields.Datetime(string='Agreement Deadline') schedule_date = fields.Date( string='Delivery Date', index=True, help= "The expected and scheduled delivery date where all the products are received" ) user_id = fields.Many2one('res.users', string='Responsible', default=lambda self: self.env.user) description = fields.Text() company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env['res.company']. _company_default_get('purchase.requisition')) purchase_ids = fields.One2many('purchase.order', 'requisition_id', string='Purchase Orders', states={'done': [('readonly', True)]}) line_ids = fields.One2many('purchase.requisition.line', 'requisition_id', string='Products to Purchase', states={'done': [('readonly', True)]}, copy=True) warehouse_id = fields.Many2one('stock.warehouse', string='Warehouse') state = fields.Selection([('draft', 'Draft'), ('in_progress', 'Confirmed'), ('open', 'Bid Selection'), ('done', 'Done'), ('cancel', 'Cancelled')], 'Status', track_visibility='onchange', required=True, copy=False, default='draft') account_analytic_id = fields.Many2one('account.analytic.account', 'Analytic Account') picking_type_id = fields.Many2one('stock.picking.type', 'Operation Type', required=True, default=_get_picking_in) @api.multi @api.depends('purchase_ids') def _compute_orders_number(self): for requisition in self: requisition.order_count = len(requisition.purchase_ids) @api.multi def action_cancel(self): # try to set all associated quotations to cancel state for requisition in self: requisition.purchase_ids.button_cancel() for po in requisition.purchase_ids: po.message_post(body=_( 'Cancelled by the agreement associated to this quotation.') ) self.write({'state': 'cancel'}) @api.multi def action_in_progress(self): if not all(obj.line_ids for obj in self): raise UserError( _('You cannot confirm call because there is no product line.')) self.write({'state': 'in_progress'}) @api.multi def action_open(self): self.write({'state': 'open'}) @api.multi def action_draft(self): self.write({'state': 'draft'}) @api.multi def action_done(self): """ Generate all purchase order based on selected lines, should only be called on one agreement at a time """ if any(purchase_order.state in ['draft', 'sent', 'to approve'] for purchase_order in self.mapped('purchase_ids')): raise UserError( _('You have to cancel or validate every RfQ before closing the purchase requisition.' )) self.write({'state': 'done'}) def _prepare_tender_values(self, product_id, product_qty, product_uom, location_id, name, origin, values): return { 'origin': origin, 'date_end': values['date_planned'], 'warehouse_id': values.get('warehouse_id') and values['warehouse_id'].id or False, 'company_id': values['company_id'].id, 'line_ids': [(0, 0, { 'product_id': product_id.id, 'product_uom_id': product_uom.id, 'product_qty': product_qty, 'move_dest_id': values.get('move_dest_ids') and values['move_dest_ids'][0].id or False, })], }
class res_partner(models.Model): _inherit = 'res.partner' # define a one2many field based on the inherited field partner_id daughter_ids = fields.One2many('test.inherit.daughter', 'partner_id')