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, readonly=True, states={'draft': [('readonly', False)]}) 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', readonly=True) 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 continue 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() if self.state == 'draft': 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 and move[0] == 0: 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.with_context(prefetch_fields=False).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}) def action_assign_partner(self): for picking in self: picking.move_lines.write({'partner_id': picking.partner_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) moves = sorted(moves, key=lambda m: m.quantity_done < m.product_qty, reverse=True) if moves: 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'] precision_digits = self.env['decimal.precision'].precision_get('Product Unit of Measure') 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(not float_is_zero(grouped_quants.get(key, 0) - grouped_ops.get(key, 0), precision_digits=precision_digits) for key in grouped_quants) \ or any(not float_is_zero(grouped_ops.get(key, 0) - grouped_quants.get(key, 0), precision_digits=precision_digits) 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 precision_digits = self.env['decimal.precision'].precision_get('Product Unit of Measure') no_quantities_done = all(float_is_zero(move_line.qty_done, precision_digits=precision_digits) for move_line in self.move_line_ids.filtered(lambda m: m.state not in ('done', 'cancel'))) 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) 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.product_uom_id._compute_quantity(pack.qty_done, pack.product_id.uom_id) 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 def action_picking_move_tree(self): action = self.env.ref('stock.stock_move_action').read()[0] action['views'] = [ (self.env.ref('stock.view_picking_move_tree').id, 'tree'), ] action['context'] = self.env.context action['domain'] = [('picking_id', 'in', self.ids)] return action
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 Flectra (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: user_id = False status = "successful" if user_id else "failed" ip = request.httprequest.environ['REMOTE_ADDR'] if request else 'n/a' _logger.info("Login %s for db:%s login:%s from %s", status, db, login, ip) 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: flectra.exceptions.AccessDenied when old password is wrong :raise: flectra.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 Contract(models.Model): _name = 'hr.contract' _description = 'Contract' _inherit = ['mail.thread'] name = fields.Char('Contract Reference', required=True) employee_id = fields.Many2one('hr.employee', string='Employee') department_id = fields.Many2one('hr.department', string="Department") type_id = fields.Many2one('hr.contract.type', string="Contract Type", required=True, default=lambda self: self.env['hr.contract.type'].search([], limit=1)) job_id = fields.Many2one('hr.job', string='Job Position') date_start = fields.Date('Start Date', required=True, default=fields.Date.today, help="Start date of the contract.") date_end = fields.Date('End Date', help="End date of the contract (if it's a fixed-term contract).") trial_date_end = fields.Date('End of Trial Period', help="End date of the trial period (if there is one).") resource_calendar_id = fields.Many2one( 'resource.calendar', 'Working Schedule', default=lambda self: self.env['res.company']._company_default_get().resource_calendar_id.id) wage = fields.Monetary('Wage', digits=(16, 2), required=True, help="Employee's monthly gross wage.") advantages = fields.Text('Advantages') notes = fields.Text('Notes') state = fields.Selection([ ('draft', 'New'), ('open', 'Running'), ('pending', 'To Renew'), ('close', 'Expired'), ('cancel', 'Cancelled') ], string='Status', group_expand='_expand_states', track_visibility='onchange', help='Status of the contract', default='draft') company_id = fields.Many2one('res.company', default=lambda self: self.env.user.company_id) currency_id = fields.Many2one(string="Currency", related='company_id.currency_id', readonly=True) permit_no = fields.Char('Work Permit No', related="employee_id.permit_no") visa_no = fields.Char('Visa No', related="employee_id.visa_no") visa_expire = fields.Date('Visa Expire Date', related="employee_id.visa_expire") def _expand_states(self, states, domain, order): return [key for key, val in type(self).state.selection] @api.onchange('employee_id') def _onchange_employee_id(self): if self.employee_id: self.job_id = self.employee_id.job_id self.department_id = self.employee_id.department_id self.resource_calendar_id = self.employee_id.resource_calendar_id @api.constrains('date_start', 'date_end') def _check_dates(self): if self.filtered(lambda c: c.date_end and c.date_start > c.date_end): raise ValidationError(_('Contract start date must be less than contract end date.')) @api.model def update_state(self): self.search([ ('state', '=', 'open'), '|', '&', ('date_end', '<=', fields.Date.to_string(date.today() + relativedelta(days=7))), ('date_end', '>=', fields.Date.to_string(date.today() + relativedelta(days=1))), '&', ('visa_expire', '<=', fields.Date.to_string(date.today() + relativedelta(days=60))), ('visa_expire', '>=', fields.Date.to_string(date.today() + relativedelta(days=1))), ]).write({ 'state': 'pending' }) self.search([ ('state', 'in', ('open', 'pending')), '|', ('date_end', '<=', fields.Date.to_string(date.today() + relativedelta(days=1))), ('visa_expire', '<=', fields.Date.to_string(date.today() + relativedelta(days=1))), ]).write({ 'state': 'close' }) return True @api.multi def _track_subtype(self, init_values): self.ensure_one() if 'state' in init_values and self.state == 'pending': return 'hr_contract.mt_contract_pending' elif 'state' in init_values and self.state == 'close': return 'hr_contract.mt_contract_close' return super(Contract, self)._track_subtype(init_values)
class AccountInvoiceReport(models.Model): _name = "account.invoice.report" _inherit = ['ir.branch.company.mixin'] _description = "Invoices Statistics" _auto = False _rec_name = 'date' @api.multi @api.depends('currency_id', 'date', 'price_total', 'price_average', 'residual') def _compute_amounts_in_user_currency(self): """Compute the amounts in the currency of the user """ context = dict(self._context or {}) user_currency_id = self.env.user.company_id.currency_id currency_rate_id = self.env['res.currency.rate'].search( [('rate', '=', 1), '|', ('company_id', '=', self.env.user.company_id.id), ('company_id', '=', False)], limit=1) base_currency_id = currency_rate_id.currency_id ctx = context.copy() for record in self: ctx['date'] = record.date record.user_currency_price_total = base_currency_id.with_context( ctx).compute(record.price_total, user_currency_id) record.user_currency_price_average = base_currency_id.with_context( ctx).compute(record.price_average, user_currency_id) record.user_currency_residual = base_currency_id.with_context( ctx).compute(record.residual, user_currency_id) date = fields.Date(readonly=True) product_id = fields.Many2one('product.product', string='Product', readonly=True) product_qty = fields.Float(string='Product Quantity', readonly=True) uom_name = fields.Char(string='Reference Unit of Measure', readonly=True) payment_term_id = fields.Many2one('account.payment.term', string='Payment Terms', oldname='payment_term', readonly=True) fiscal_position_id = fields.Many2one('account.fiscal.position', oldname='fiscal_position', string='Fiscal Position', readonly=True) currency_id = fields.Many2one('res.currency', string='Currency', readonly=True) categ_id = fields.Many2one('product.category', string='Product Category', readonly=True) journal_id = fields.Many2one('account.journal', string='Journal', readonly=True) partner_id = fields.Many2one('res.partner', string='Partner', readonly=True) commercial_partner_id = fields.Many2one('res.partner', string='Partner Company', help="Commercial Entity") company_id = fields.Many2one('res.company', string='Company', readonly=True) user_id = fields.Many2one('res.users', string='Salesperson', readonly=True) price_total = fields.Float(string='Total Without Tax', readonly=True) user_currency_price_total = fields.Float( string="Total Without Tax", compute='_compute_amounts_in_user_currency', digits=0) price_average = fields.Float(string='Average Price', readonly=True, group_operator="avg") user_currency_price_average = fields.Float( string="Average Price", compute='_compute_amounts_in_user_currency', digits=0) currency_rate = fields.Float(string='Currency Rate', readonly=True, group_operator="avg", groups="base.group_multi_currency") nbr = fields.Integer( string='# of Lines', readonly=True) # TDE FIXME master: rename into nbr_lines type = fields.Selection([ ('out_invoice', 'Customer Invoice'), ('in_invoice', 'Vendor Bill'), ('out_refund', 'Customer Credit Note'), ('in_refund', 'Vendor Credit Note'), ], readonly=True) state = fields.Selection([('draft', 'Draft'), ('open', 'Open'), ('paid', 'Paid'), ('cancel', 'Cancelled')], string='Invoice Status', readonly=True) date_due = fields.Date(string='Due Date', readonly=True) account_id = fields.Many2one('account.account', string='Account', readonly=True, domain=[('deprecated', '=', False)]) account_line_id = fields.Many2one('account.account', string='Account Line', readonly=True, domain=[('deprecated', '=', False)]) partner_bank_id = fields.Many2one('res.partner.bank', string='Bank Account', readonly=True) residual = fields.Float(string='Due Amount', readonly=True) user_currency_residual = fields.Float( string="Total Residual", compute='_compute_amounts_in_user_currency', digits=0) country_id = fields.Many2one('res.country', string='Country of the Partner Company') account_analytic_id = fields.Many2one( 'account.analytic.account', string='Analytic Account', groups="analytic.group_analytic_accounting") _order = 'date desc' _depends = { 'account.invoice': [ 'account_id', 'amount_total_company_signed', 'commercial_partner_id', 'company_id', 'branch_id', 'currency_id', 'date_due', 'date_invoice', 'fiscal_position_id', 'journal_id', 'partner_bank_id', 'partner_id', 'payment_term_id', 'residual', 'state', 'type', 'user_id', ], 'account.invoice.line': [ 'account_id', 'invoice_id', 'price_subtotal', 'product_id', 'quantity', 'uom_id', 'account_analytic_id', ], 'product.product': ['product_tmpl_id'], 'product.template': ['categ_id'], 'product.uom': ['category_id', 'factor', 'name', 'uom_type'], 'res.currency.rate': ['currency_id', 'name'], 'res.partner': ['country_id'], } def _select(self): select_str = """ SELECT sub.id, sub.date, sub.product_id, sub.partner_id, sub.country_id, sub.account_analytic_id, sub.payment_term_id, sub.uom_name, sub.currency_id, sub.journal_id, sub.fiscal_position_id, sub.user_id, sub.company_id, sub.branch_id, sub.nbr, sub.type, sub.state, sub.categ_id, sub.date_due, sub.account_id, sub.account_line_id, sub.partner_bank_id, sub.product_qty, sub.price_total as price_total, sub.price_average as price_average, COALESCE(cr.rate, 1) as currency_rate, sub.residual as residual, sub.commercial_partner_id as commercial_partner_id """ return select_str def _sub_select(self): select_str = """ SELECT ail.id AS id, ai.date_invoice AS date, ail.product_id, ai.partner_id, ai.payment_term_id, ail.account_analytic_id, u2.name AS uom_name, ai.currency_id, ai.journal_id, ai.fiscal_position_id, ai.user_id, ai.company_id, ai.branch_id, 1 AS nbr, ai.type, ai.state, pt.categ_id, ai.date_due, ai.account_id, ail.account_id AS account_line_id, ai.partner_bank_id, SUM ((invoice_type.sign * ail.quantity) / u.factor * u2.factor) AS product_qty, SUM(ail.price_subtotal_signed * invoice_type.sign) AS price_total, SUM(ABS(ail.price_subtotal_signed)) / CASE WHEN SUM(ail.quantity / u.factor * u2.factor) <> 0::numeric THEN SUM(ail.quantity / u.factor * u2.factor) ELSE 1::numeric END AS price_average, ai.residual_company_signed / (SELECT count(*) FROM account_invoice_line l where invoice_id = ai.id) * count(*) * invoice_type.sign AS residual, ai.commercial_partner_id as commercial_partner_id, partner.country_id """ return select_str def _from(self): from_str = """ FROM account_invoice_line ail JOIN account_invoice ai ON ai.id = ail.invoice_id JOIN res_partner partner ON ai.commercial_partner_id = partner.id LEFT JOIN product_product pr ON pr.id = ail.product_id left JOIN product_template pt ON pt.id = pr.product_tmpl_id LEFT JOIN product_uom u ON u.id = ail.uom_id LEFT JOIN product_uom u2 ON u2.id = pt.uom_id JOIN ( -- Temporary table to decide if the qty should be added or retrieved (Invoice vs Credit Note) SELECT id,(CASE WHEN ai.type::text = ANY (ARRAY['in_refund'::character varying::text, 'in_invoice'::character varying::text]) THEN -1 ELSE 1 END) AS sign FROM account_invoice ai ) AS invoice_type ON invoice_type.id = ai.id """ return from_str def _group_by(self): group_by_str = """ GROUP BY ail.id, ail.product_id, ail.account_analytic_id, ai.date_invoice, ai.id, ai.partner_id, ai.payment_term_id, u2.name, u2.id, ai.currency_id, ai.journal_id, ai.fiscal_position_id, ai.user_id, ai.company_id, ai.branch_id, ai.type, invoice_type.sign, ai.state, pt.categ_id, ai.date_due, ai.account_id, ail.account_id, ai.partner_bank_id, ai.residual_company_signed, ai.amount_total_company_signed, ai.commercial_partner_id, partner.country_id """ return group_by_str @api.model_cr def init(self): # self._table = account_invoice_report tools.drop_view_if_exists(self.env.cr, self._table) self.env.cr.execute( """CREATE or REPLACE VIEW %s as ( WITH currency_rate AS (%s) %s FROM ( %s %s %s ) AS sub LEFT JOIN currency_rate cr ON (cr.currency_id = sub.currency_id AND cr.company_id = sub.company_id AND cr.date_start <= COALESCE(sub.date, NOW()) AND (cr.date_end IS NULL OR cr.date_end > COALESCE(sub.date, NOW()))) )""" % (self._table, self.env['res.currency']._select_companies_rates(), self._select(), self._sub_select(), self._from(), self._group_by()))
class Users(models.Model): """ Update of res.users class - add a preference about sending emails about notifications - make a new user follow itself - add a welcome message - add suggestion preference - if adding groups to an user, check mail.channels linked to this user group, and the user. This is done by overriding the write method. """ _name = 'res.users' _inherit = ['res.users'] alias_id = fields.Many2one('mail.alias', 'Alias', ondelete="set null", required=False, help="Email address internally associated with this user. Incoming "\ "emails will appear in the user's notifications.", copy=False, auto_join=True) alias_contact = fields.Selection([ ('everyone', 'Everyone'), ('partners', 'Authenticated Partners'), ('followers', 'Followers only')], string='Alias Contact Security', related='alias_id.alias_contact') notification_type = fields.Selection([ ('email', 'Handle by Emails'), ('inbox', 'Handle in Flectra')], 'Notification Management', required=True, default='email', help="Policy on how to handle Chatter notifications:\n" "- Emails: notifications are sent to your email\n" "- Flectra: notifications appear in your Flectra Inbox") def __init__(self, pool, cr): """ Override of __init__ to add access rights on notification_email_send and alias fields. Access rights are disabled by default, but allowed on some specific fields defined in self.SELF_{READ/WRITE}ABLE_FIELDS. """ init_res = super(Users, self).__init__(pool, cr) # duplicate list to avoid modifying the original reference type(self).SELF_WRITEABLE_FIELDS = list(self.SELF_WRITEABLE_FIELDS) type(self).SELF_WRITEABLE_FIELDS.extend(['notification_type']) # duplicate list to avoid modifying the original reference type(self).SELF_READABLE_FIELDS = list(self.SELF_READABLE_FIELDS) type(self).SELF_READABLE_FIELDS.extend(['notification_type']) return init_res @api.model def create(self, values): if not values.get('login', False): action = self.env.ref('base.action_res_users') msg = _("You cannot create a new user from here.\n To create new user please go to configuration panel.") raise exceptions.RedirectWarning(msg, action.id, _('Go to the configuration panel')) user = super(Users, self).create(values) # create a welcome message user._create_welcome_message() return user @api.multi def write(self, vals): write_res = super(Users, self).write(vals) if vals.get('groups_id'): # form: {'group_ids': [(3, 10), (3, 3), (4, 10), (4, 3)]} or {'group_ids': [(6, 0, [ids]} user_group_ids = [command[1] for command in vals['groups_id'] if command[0] == 4] user_group_ids += [id for command in vals['groups_id'] if command[0] == 6 for id in command[2]] self.env['mail.channel'].search([('group_ids', 'in', user_group_ids)])._subscribe_users() return write_res def _create_welcome_message(self): self.ensure_one() if not self.has_group('base.group_user'): return False company_name = self.company_id.name if self.company_id else '' body = _('%s has joined the %s network.') % (self.name, company_name) # TODO change SUPERUSER_ID into user.id but catch errors return self.partner_id.sudo().message_post(body=body) def _message_post_get_pid(self): self.ensure_one() if 'thread_model' in self.env.context: self = self.with_context(thread_model='res.users') return self.partner_id.id @api.multi @api.returns('self', lambda value: value.id) def message_post(self, **kwargs): """ Redirect the posting of message on res.users as a private discussion. This is done because when giving the context of Chatter on the various mailboxes, we do not have access to the current partner_id. """ current_pids = [] partner_ids = kwargs.get('partner_ids', []) user_pid = self._message_post_get_pid() for partner_id in partner_ids: if isinstance(partner_id, (list, tuple)) and partner_id[0] == 4 and len(partner_id) == 2: current_pids.append(partner_id[1]) elif isinstance(partner_id, (list, tuple)) and partner_id[0] == 6 and len(partner_id) == 3: current_pids.append(partner_id[2]) elif isinstance(partner_id, pycompat.integer_types): current_pids.append(partner_id) if user_pid not in current_pids: partner_ids.append(user_pid) kwargs['partner_ids'] = partner_ids return self.env['mail.thread'].message_post(**kwargs) def message_update(self, msg_dict, update_vals=None): return True def message_subscribe(self, partner_ids=None, channel_ids=None, subtype_ids=None, force=True): return True @api.multi def message_partner_info_from_emails(self, emails, link_mail=False): return self.env['mail.thread'].message_partner_info_from_emails(emails, link_mail=link_mail) @api.multi def message_get_suggested_recipients(self): return dict((res_id, list()) for res_id in self._ids) @api.model def activity_user_count(self): query = """SELECT m.name, count(*), act.res_model as model, CASE WHEN now()::date - act.date_deadline::date = 0 Then 'today' WHEN now()::date - act.date_deadline::date > 0 Then 'overdue' WHEN now()::date - act.date_deadline::date < 0 Then 'planned' END AS states FROM mail_activity AS act JOIN ir_model AS m ON act.res_model_id = m.id WHERE user_id = %s GROUP BY m.name, states, act.res_model; """ self.env.cr.execute(query, [self.env.uid]) activity_data = self.env.cr.dictfetchall() user_activities = {} for activity in activity_data: if not user_activities.get(activity['model']): user_activities[activity['model']] = { 'name': activity['name'], 'model': activity['model'], 'icon': modules.module.get_module_icon(self.env[activity['model']]._original_module), 'total_count': 0, 'today_count': 0, 'overdue_count': 0, 'planned_count': 0, } user_activities[activity['model']]['%s_count' % activity['states']] += activity['count'] if activity['states'] in ('today','overdue'): user_activities[activity['model']]['total_count'] += activity['count'] return list(user_activities.values())
class AccountInvoiceLine(models.Model): _inherit = 'account.move.line' asset_category_id = fields.Many2one('account.asset.category', string='Asset Category') asset_start_date = fields.Date(string='Asset Start Date', compute='_get_asset_date', readonly=True, store=True) asset_end_date = fields.Date(string='Asset End Date', compute='_get_asset_date', readonly=True, store=True) asset_mrr = fields.Float(string='Monthly Recurring Revenue', compute='_get_asset_date', readonly=True, digits="Account", store=True) @api.depends('asset_category_id', 'move_id.invoice_date') def _get_asset_date(self): for rec in self: rec.asset_mrr = 0 rec.asset_start_date = False rec.asset_end_date = False cat = rec.asset_category_id if cat: if cat.method_number == 0 or cat.method_period == 0: raise UserError( _('The number of depreciations or the period length of ' 'your asset category cannot be 0.')) months = cat.method_number * cat.method_period if rec.move_id.move_type in ['out_invoice', 'out_refund']: rec.asset_mrr = rec.price_subtotal / months if rec.move_id.invoice_date: start_date = rec.move_id.invoice_date.replace(day=1) end_date = (start_date + relativedelta(months=months, days=-1)) rec.asset_start_date = start_date rec.asset_end_date = end_date def asset_create(self): if self.asset_category_id: vals = { 'name': self.name, 'code': self.name or False, 'category_id': self.asset_category_id.id, 'value': self.price_subtotal, 'partner_id': self.move_id.partner_id.id, 'company_id': self.move_id.company_id.id, 'currency_id': self.move_id.company_currency_id.id, 'date': self.move_id.invoice_date, 'invoice_id': self.move_id.id, } changed_vals = self.env[ 'account.asset.asset'].onchange_category_id_values( vals['category_id']) vals.update(changed_vals['value']) asset = self.env['account.asset.asset'].create(vals) if self.asset_category_id.open_asset: asset.validate() return True @api.onchange('asset_category_id') def onchange_asset_category_id(self): if self.move_id.move_type == 'out_invoice' and self.asset_category_id: self.account_id = self.asset_category_id.account_asset_id.id elif self.move_id.move_type == 'in_invoice' and self.asset_category_id: self.account_id = self.asset_category_id.account_asset_id.id @api.onchange('uom_id') def _onchange_uom_id(self): result = super(AccountInvoiceLine, self)._onchange_uom_id() self.onchange_asset_category_id() return result @api.onchange('product_id') def _onchange_product_id(self): vals = super(AccountInvoiceLine, self)._onchange_product_id() if self.product_id: for rec in self: if rec.move_id.move_type == 'out_invoice': rec.asset_category_id = rec.product_id.product_tmpl_id.deferred_revenue_category_id elif rec.move_id.move_type == 'in_invoice': rec.asset_category_id = rec.product_id.product_tmpl_id.asset_category_id return vals def _set_additional_fields(self, invoice): if not self.asset_category_id: if invoice.type == 'out_invoice': self.asset_category_id = self.product_id.product_tmpl_id.deferred_revenue_category_id.id elif invoice.type == 'in_invoice': self.asset_category_id = self.product_id.product_tmpl_id.asset_category_id.id self.onchange_asset_category_id() super(AccountInvoiceLine, self)._set_additional_fields(invoice) def get_invoice_line_account(self, type, product, fpos, company): return product.asset_category_id.account_asset_id or super( AccountInvoiceLine, self).get_invoice_line_account( type, product, fpos, company)
class ResPartner(models.Model): _inherit = 'res.partner' contact_status_id = fields.Many2one('crm.contacts.status', 'Contact Status') contact_type_id = fields.Many2one('crm.contacts.type', 'Contact Type')
class userSignature(models.Model): # _name = 'user.signature.key' _inherit = 'res.users' def default_status(self): return 'unverified' def load_cert_m2pem(self, *args, **kwargs): filecontent = base64.b64decode(self.key_file) cert = M2X509.load_cert(filecontent) issuer = cert.get_issuer() subject = cert.get_subject() def load_cert_pk12(self, filecontent): p12 = crypto.load_pkcs12(filecontent, self.dec_pass) cert = p12.get_certificate() privky = p12.get_privatekey() cacert = p12.get_ca_certificates() issuer = cert.get_issuer() subject = cert.get_subject() self.not_before = datetime.datetime.strptime(cert.get_notBefore().decode("utf-8"), '%Y%m%d%H%M%SZ') self.not_after = datetime.datetime.strptime(cert.get_notAfter().decode("utf-8"), '%Y%m%d%H%M%SZ') # self.final_date = self.subject_c = subject.C self.subject_title = subject.title self.subject_common_name = subject.CN self.subject_serial_number = subject.serialNumber self.subject_email_address = subject.emailAddress self.issuer_country = issuer.C self.issuer_organization = issuer.O self.issuer_common_name = issuer.CN self.issuer_serial_number = issuer.serialNumber self.issuer_email_address = issuer.emailAddress self.status = 'expired' if cert.has_expired() else 'valid' self.cert_serial_number = cert.get_serial_number() self.cert_signature_algor = cert.get_signature_algorithm() self.cert_version = cert.get_version() self.cert_hash = cert.subject_name_hash() # data privada self.private_key_bits = privky.bits() self.private_key_check = privky.check() self.private_key_type = privky.type() # self.cacert = cacert certificate = p12.get_certificate() private_key = p12.get_privatekey() self.priv_key = crypto.dump_privatekey(type_, private_key) self.cert = crypto.dump_certificate(type_, certificate) pubkey = cert.get_pubkey() filename = fields.Char(string='File Name') key_file = fields.Binary( string='Signature File', required=False, store=True, help='Upload the Signature File') dec_pass = fields.Char(string='Pasword') # vigencia y estado not_before = fields.Date( string='Not Before', help='Not Before this Date', readonly=True) not_after = fields.Date( string='Not After', help='Not After this Date', readonly=True) status = fields.Selection( [('unverified', 'Unverified'), ('valid', 'Valid'), ('expired', 'Expired')], string='Status', default=default_status, help='''Draft: means it has not been checked yet.\nYou must press the\ "check" button.''') final_date = fields.Date( string='Last Date', help='Last Control Date', readonly=True) # sujeto subject_title = fields.Char(string='Subject Title', readonly=True) subject_c = fields.Char(string='Subject Country', readonly=True) subject_serial_number = fields.Char( string='Subject Serial Number') subject_common_name = fields.Char( string='Subject Common Name', readonly=True) subject_email_address = fields.Char( string='Subject Email Address', readonly=True) # emisor issuer_country = fields.Char(string='Issuer Country', readonly=True) issuer_serial_number = fields.Char( string='Issuer Serial Number', readonly=True) issuer_common_name = fields.Char( string='Issuer Common Name', readonly=True) issuer_email_address = fields.Char( string='Issuer Email Address', readonly=True) issuer_organization = fields.Char( string='Issuer Organization', readonly=True) # data del certificado cert_serial_number = fields.Char(string='Serial Number', readonly=True) cert_signature_algor = fields.Char(string='Signature Algorithm', readonly=True) cert_version = fields.Char(string='Version', readonly=True) cert_hash = fields.Char(string='Hash', readonly=True) # data privad, readonly=Truea private_key_bits = fields.Char(string='Private Key Bits', readonly=True) private_key_check = fields.Char(string='Private Key Check', readonly=True) private_key_type = fields.Char(string='Private Key Type', readonly=True) # cacert = fields.Char('CA Cert', readonly=True) cert = fields.Text(string='Certificate', readonly=True) priv_key = fields.Text('Private Key', readonly=True) authorized_users_ids = fields.One2many('res.users','cert_owner_id', string='Authorized Users') cert_owner_id = fields.Many2one('res.users', string='Certificate Owner', select=True, ondelete='cascade') @api.multi def action_clean1(self): self.ensure_one() # todo: debe lanzar un wizard que confirme si se limpia o no # self.status = 'unverified' self.write(zero_values) @api.multi def action_process(self): self.ensure_one() filecontent = base64.b64decode(self.key_file) self.load_cert_pk12(filecontent) @api.multi @api.depends('key_file') def _get_date(self): self.ensure_one() old_date = self.issued_date if self.key_file != None and self.status == 'unverified': print(self.key_file) self.issued_date = fields.datetime.now() else: print('valor antiguo de fecha') print(old_date) self.issued_date = old_date
class WkFeed(models.Model): _name = "wk.feed" sequence = fields.Char(string='Sequence', ) active = fields.Boolean(string='Active', default=1) name = fields.Char(string='Name', ) __last_update = fields.Datetime(string='Last Modified on') state = fields.Selection( selection=State, string='Update Required', default='draft', copy=False, ) channel_id = fields.Many2one('multi.channel.sale', string='Instance', domain=[('state', '=', 'validate')]) channel = fields.Selection( related='channel_id.channel', string="Channel", ) store_id = fields.Char(string='Store ID', ) store_source = fields.Char(string='Store Source', ) message = fields.Html( string='Message', copy=False, ) @api.model def get_product_fields(self): return ProductFields @api.model def get_category_fields(self): return CategoryFields @api.multi def open_mapping_view(self): self.ensure_one() res_model = self._context.get('mapping_model') store_field = self._context.get('store_field') duplicity_domain = [] channel_id = self.channel_id duplicity_config = self.env['multi.channel.sale.config'].get_values() domain = [ ('channel_id', '=', channel_id.id), ] if res_model in [ 'channel.product.mappings', 'channel.template.mappings' ] and duplicity_config.get('avoid_duplicity'): avoid_duplicity_using = duplicity_config.get( 'avoid_duplicity_using') barcode = self.barcode default_code = self.default_code domain += ['|', (store_field, '=', self.store_id)] if default_code and barcode: domain += [ '|', ('barcode', '=', barcode), ('default_code', '=', default_code) ] elif default_code: domain += [('default_code', '=', default_code)] else: domain += [('barcode', '=', barcode)] else: domain += [(store_field, '=', self.store_id)] mapping_ids = self.env[res_model].search(domain).ids return { 'name': ('Mapping'), 'type': 'ir.actions.act_window', 'view_type': 'form', 'view_mode': 'tree,form', 'res_model': res_model, 'view_id': False, 'domain': [('id', 'in', mapping_ids)], 'target': 'current', } @api.multi def set_feed_state(self, state='done'): self.state = state return True @api.multi def get_feed_result(self, feed_type): message = "" tot = len(self) if tot == 1: if self.state == 'done': message += "{_type} feed sucessfully evaluated .".format( _type=feed_type) else: message += "{_type} feed failed to evaluate .".format( _type=feed_type) else: done_feeds = self.filtered(lambda feed: feed.state == 'done') error_feed = tot - len(done_feeds) if not error_feed: message += "All ({done}) {_type} feed sucessfully evaluated .".format( done=len(done_feeds), _type=feed_type) else: message += "<br/>{error} {_type} feed failed to evaluate".format( error=error_feed, _type=feed_type) if len(done_feeds): message += "<br/>{done} {_type} feed sucessfully evaluated".format( done=len(done_feeds), _type=feed_type) return message @api.model def get_channel_domain(self): return [('channel_id', '=', self.channel_id.id)] @api.model def get_categ_id(self, store_categ_id, channel_id): message = '' categ_id = None match = channel_id.match_category_mappings(store_categ_id) if match: categ_id = match.flectra_category_id else: feed = channel_id.match_category_feeds(store_categ_id) if feed: res = feed.import_category(channel_id) message += res.get('message', '') mapping_id = res.get('update_id') or res.get('create_id') if mapping_id: categ_id = mapping_id.flectra_category_id else: message += '<br/>Category Feed Error: No mapping as well category feed found for %s .' % ( store_categ_id) return dict(categ_id=categ_id, message=message) @api.model def get_extra_categ_ids(self, store_categ_ids, channel_id): message = '' categ_ids = [] for store_categ_id in store_categ_ids.strip(',').split(','): res = self.get_categ_id(store_categ_id, channel_id) message += res.get('message', '') categ_id = res.get('categ_id') if categ_id: categ_ids += [categ_id] return dict(categ_ids=categ_ids, message=message) @api.model def get_order_partner_id(self, store_partner_id, channel_id): partner_obj = self.env['res.partner'] message = '' partner_id = None partner_invoice_id = None partner_shipping_id = None context = dict(self._context) context['no_mapping'] = self.customer_is_guest partner_id = self.with_context(context).create_partner_contact_id( partner_id, channel_id, store_partner_id) partner_invoice_id = self.with_context( context).create_partner_invoice_id(partner_id, channel_id, self.invoice_partner_id) if self.same_shipping_billing: partner_shipping_id = partner_invoice_id else: partner_shipping_id = self.with_context( context).create_partner_shipping_id(partner_id, channel_id, partner_shipping_id) return dict(partner_id=partner_id, partner_shipping_id=partner_shipping_id, partner_invoice_id=partner_invoice_id, message=message) @api.model def get_partner_id(self, store_partner_id, _type='contact', channel_id=None): partner_obj = self.env['res.partner'] message = '' partner_id = None match = channel_id.match_partner_mappings(store_partner_id, _type) if match: partner_id = match.flectra_partner else: feed = channel_id.match_partner_feeds(store_partner_id, _type) if feed: res = feed.import_partner(channel_id) message += res.get('message', '') mapping_id = res.get('update_id') or res.get('create_id') if mapping_id: partner_id = mapping_id.flectra_partner else: message += '<br/>Partner Feed Error: No mapping as well partner feed found for %s.' % ( store_partner_id) return dict(partner_id=partner_id, message=message) @api.model def get_product_id(self, store_product_id, line_variant_ids, channel_id, default_code=None, barcode=None): message = '' domain = [] if default_code: domain += [ ('default_code', '=', default_code), ] if barcode: domain += [ ('barcode', '=', barcode), ] product_id = None match = channel_id.match_product_mappings(store_product_id, line_variant_ids, domain=domain) if match: product_id = match.product_name else: feed = channel_id.match_product_feeds(store_product_id, domain=domain) product_variant_feeds = channel_id.match_product_variant_feeds( store_product_id, domain=domain) if feed: res = feed.import_product(channel_id) message += res.get('message', '') mapping_id = res.get('update_id') or res.get('create_id') match = channel_id.match_product_mappings(store_product_id, line_variant_ids, domain=domain) if match: product_id = match.product_name elif product_variant_feeds and product_variant_feeds.feed_templ_id: res = product_variant_feeds.feed_templ_id.import_product( channel_id) message += res.get('message', '') match = channel_id.match_product_mappings( line_variant_ids=store_product_id) if match: product_id = match.product_name else: message += '<br/>Product Feed Error: For product id (%s) sku (%s) no mapping as well feed found.' % ( store_product_id, default_code) return dict(product_id=product_id, message=message) @api.model def get_carrier_id(self, carrier_id, service_id=None, channel_id=None): message = '' res_id = None shipping_service_name = service_id and service_id or carrier_id match = channel_id.match_carrier_mappings(shipping_service_name) if match: res_id = match.flectra_shipping_carrier else: res_id = channel_id.create_carrier_mapping(carrier_id, service_id) return dict(carrier_id=res_id, message=message) def get_partner_invoice_vals(self, partner_id, channel_id): name = self.invoice_name if self.invoice_last_name: name = '%s %s' % (name, self.invoice_last_name) vals = dict( type='invoice', name=self.invoice_name, street=self.invoice_street, street2=self.invoice_street2, city=self.invoice_city, zip=self.invoice_zip, email=self.invoice_email, phone=self.invoice_phone, mobile=self.invoice_mobile, parent_id=partner_id.id, customer=False, ) country_id = self.invoice_country_id and channel_id.get_country_id( self.invoice_country_id) if country_id: vals['country_id'] = country_id.id state_id = (self.invoice_state_id or self.invoice_state_name ) and country_id and channel_id.get_state_id( self.invoice_state_id, country_id, self.invoice_state_name) if state_id: vals['state_id'] = state_id.id return vals @api.model def create_partner_invoice_id(self, partner_id, channel_id, invoice_partner_id=None): partner_obj = self.env['res.partner'] vals = self.get_partner_invoice_vals(partner_id, channel_id) match = None if invoice_partner_id: match = channel_id.match_partner_mappings(invoice_partner_id, 'invoice') if match: match.flectra_partner.write(vals) erp_id = match.flectra_partner else: erp_id = partner_obj.create(vals) if (not self._context.get('no_mapping') and invoice_partner_id): channel_id.create_partner_mapping(erp_id, invoice_partner_id, 'invoice') return erp_id def get_partner_shipping_vals(self, partner_id, channel_id): name = self.shipping_name if self.shipping_last_name: name = '%s %s' % (name, self.shipping_last_name) vals = dict( type='delivery', name=self.shipping_name, street=self.shipping_street, street2=self.shipping_street2, city=self.shipping_city, zip=self.shipping_zip, email=self.shipping_email, phone=self.shipping_phone, mobile=self.shipping_mobile, parent_id=partner_id.id, customer=False, ) country_id = self.shipping_country_id and channel_id.get_country_id( self.shipping_country_id) if country_id: vals['country_id'] = country_id.id state_id = (self.shipping_state_id or self.shipping_state_name ) and country_id and channel_id.get_state_id( self.shipping_state_id, country_id, self.shipping_state_name) if state_id: vals['state_id'] = state_id.id return vals @api.model def create_partner_shipping_id(self, partner_id, channel_id, shipping_partner_id=None): partner_obj = self.env['res.partner'] match = None vals = self.get_partner_shipping_vals(partner_id, channel_id) if shipping_partner_id: match = channel_id.match_partner_mappings(shipping_partner_id, 'delivery') if match: match.flectra_partner.write(vals) erp_id = match.flectra_partner else: erp_id = partner_obj.create(vals) if (not self._context.get('no_mapping') and shipping_partner_id): channel_id.create_partner_mapping(erp_id, shipping_partner_id, 'delivery') return erp_id def get_partner_contact_vals(self, partner_id, channel_id): _type = 'contact' name = self.customer_name if self.customer_last_name: name = '%s %s' % (name, self.customer_last_name) vals = dict( type=_type, customer=1, name=self.customer_name, email=self.customer_email, phone=self.customer_phone, mobile=self.customer_mobile, ) return vals @api.model def create_partner_contact_id(self, partner_id, channel_id, store_partner_id=None): partner_obj = self.env['res.partner'] vals = self.get_partner_contact_vals(partner_id, channel_id) match = None if store_partner_id: match = channel_id.match_partner_mappings(store_partner_id, 'contact') if match: match.flectra_partner.write(vals) erp_id = match.flectra_partner else: erp_id = partner_obj.create(vals) if (not self._context.get('no_mapping') and store_partner_id): channel_id.create_partner_mapping(erp_id, store_partner_id, 'contact') return erp_id
class RestaurantTable(models.Model): _name = 'restaurant.table' name = fields.Char('Table Name', required=True, help='An internal identification of a table') floor_id = fields.Many2one('restaurant.floor', string='Floor') shape = fields.Selection([('square', 'Square'), ('round', 'Round')], string='Shape', required=True, default='square') position_h = fields.Float( 'Horizontal Position', default=10, help= "The table's horizontal position from the left side to the table's center, in pixels" ) position_v = fields.Float( 'Vertical Position', default=10, help= "The table's vertical position from the top to the table's center, in pixels" ) width = fields.Float('Width', default=50, help="The table's width in pixels") height = fields.Float('Height', default=50, help="The table's height in pixels") seats = fields.Integer( 'Seats', default=1, help="The default number of customer served at this table.") color = fields.Char( 'Color', help= "The table's color, expressed as a valid 'background' CSS property value" ) active = fields.Boolean( 'Active', default=True, help= 'If false, the table is deactivated and will not be available in the point of sale' ) @api.model def create_from_ui(self, table): """ create or modify a table from the point of sale UI. table contains the table's fields. If it contains an id, it will modify the existing table. It then returns the id of the table. """ if table.get('floor_id'): table['floor_id'] = table['floor_id'][0] table_id = table.pop('id', False) if table_id: self.browse(table_id).write(table) else: table_id = self.create(table).id return table_id
class ResConfigSettings(models.TransientModel): _inherit = 'res.config.settings' default_template_id = fields.Many2one('sale.quote.template', default_model='sale.order', string='Default Template')
class Goal(models.Model): """Goal instance for a user An individual goal for a user on a specified time period""" _name = 'gamification.goal' _description = 'Gamification goal instance' _order = 'start_date desc, end_date desc, definition_id, id' definition_id = fields.Many2one('gamification.goal.definition', string="Goal Definition", required=True, ondelete="cascade") user_id = fields.Many2one('res.users', string="User", required=True, auto_join=True, ondelete="cascade") line_id = fields.Many2one('gamification.challenge.line', string="Challenge Line", ondelete="cascade") challenge_id = fields.Many2one( related='line_id.challenge_id', store=True, readonly=True, help="Challenge that generated the goal, assign challenge to users " "to generate goals with a value in this field.") start_date = fields.Date("Start Date", default=fields.Date.today) end_date = fields.Date("End Date") # no start and end = always active target_goal = fields.Float('To Reach', required=True, track_visibility='always') # no goal = global index current = fields.Float("Current Value", required=True, default=0, track_visibility='always') completeness = fields.Float("Completeness", compute='_get_completion') state = fields.Selection([ ('draft', "Draft"), ('inprogress', "In progress"), ('reached', "Reached"), ('failed', "Failed"), ('canceled', "Canceled"), ], default='draft', string='State', required=True, track_visibility='always') to_update = fields.Boolean('To update') closed = fields.Boolean('Closed goal', help="These goals will not be recomputed.") computation_mode = fields.Selection( related='definition_id.computation_mode') remind_update_delay = fields.Integer( "Remind delay", help="The number of days after which the user " "assigned to a manual goal will be reminded. " "Never reminded if no value is specified.") last_update = fields.Date( "Last Update", help="In case of manual goal, reminders are sent if the goal as not " "been updated for a while (defined in challenge). Ignored in " "case of non-manual goal or goal not linked to a challenge.") definition_description = fields.Text("Definition Description", related='definition_id.description', readonly=True) definition_condition = fields.Selection("Definition Condition", related='definition_id.condition', readonly=True) definition_suffix = fields.Char("Suffix", related='definition_id.full_suffix', readonly=True) definition_display = fields.Selection("Display Mode", related='definition_id.display_mode', readonly=True) @api.depends('current', 'target_goal', 'definition_id.condition') def _get_completion(self): """Return the percentage of completeness of the goal, between 0 and 100""" for goal in self: if goal.definition_condition == 'higher': if goal.current >= goal.target_goal: goal.completeness = 100.0 else: goal.completeness = round( 100.0 * goal.current / goal.target_goal, 2) elif goal.current < goal.target_goal: # a goal 'lower than' has only two values possible: 0 or 100% goal.completeness = 100.0 else: goal.completeness = 0.0 def _check_remind_delay(self): """Verify if a goal has not been updated for some time and send a reminder message of needed. :return: data to write on the goal object """ if not (self.remind_update_delay and self.last_update): return {} delta_max = timedelta(days=self.remind_update_delay) last_update = fields.Date.from_string(self.last_update) if date.today() - last_update < delta_max: return {} # generate a reminder report template = self.env.ref('gamification.email_template_goal_reminder')\ .get_email_template(self.id) body_html = self.env['mail.template'].with_context(template._context)\ .render_template(template.body_html, 'gamification.goal', self.id) self.env['mail.thread'].message_post( body=body_html, partner_ids=[self.user_id.partner_id.id], subtype='mail.mt_comment') return {'to_update': True} def _get_write_values(self, new_value): """Generate values to write after recomputation of a goal score""" if new_value == self.current: # avoid useless write if the new value is the same as the old one return {} result = {'current': new_value} if (self.definition_id.condition == 'higher' and new_value >= self.target_goal) \ or (self.definition_id.condition == 'lower' and new_value <= self.target_goal): # success, do no set closed as can still change result['state'] = 'reached' elif self.end_date and fields.Date.today() > self.end_date: # check goal failure result['state'] = 'failed' result['closed'] = True return {self: result} @api.multi def update_goal(self): """Update the goals to recomputes values and change of states If a manual goal is not updated for enough time, the user will be reminded to do so (done only once, in 'inprogress' state). If a goal reaches the target value, the status is set to reached If the end date is passed (at least +1 day, time not considered) without the target value being reached, the goal is set as failed.""" goals_by_definition = {} for goal in self: goals_by_definition.setdefault(goal.definition_id, []).append(goal) for definition, goals in goals_by_definition.items(): goals_to_write = {} if definition.computation_mode == 'manually': for goal in goals: goals_to_write[goal] = goal._check_remind_delay() elif definition.computation_mode == 'python': # TODO batch execution for goal in goals: # execute the chosen method cxt = { 'object': goal, 'env': self.env, 'date': date, 'datetime': datetime, 'timedelta': timedelta, 'time': time, } code = definition.compute_code.strip() safe_eval(code, cxt, mode="exec", nocopy=True) # the result of the evaluated codeis put in the 'result' local variable, propagated to the context result = cxt.get('result') if result is not None and isinstance( result, (float, pycompat.integer_types)): goals_to_write.update(goal._get_write_values(result)) else: _logger.error( "Invalid return content '%r' from the evaluation " "of code for definition %s, expected a number", result, definition.name) else: # count or sum Obj = self.env[definition.model_id.model] field_date_name = definition.field_date_id.name if definition.computation_mode == 'count' and definition.batch_mode: # batch mode, trying to do as much as possible in one request general_domain = safe_eval(definition.domain) field_name = definition.batch_distinctive_field.name subqueries = {} for goal in goals: start_date = field_date_name and goal.start_date or False end_date = field_date_name and goal.end_date or False subqueries.setdefault( (start_date, end_date), {}).update({ goal.id: safe_eval(definition.batch_user_expression, {'user': goal.user_id}) }) # the global query should be split by time periods (especially for recurrent goals) for (start_date, end_date), query_goals in subqueries.items(): subquery_domain = list(general_domain) subquery_domain.append( (field_name, 'in', list(set(query_goals.values())))) if start_date: subquery_domain.append( (field_date_name, '>=', start_date)) if end_date: subquery_domain.append( (field_date_name, '<=', end_date)) if field_name == 'id': # grouping on id does not work and is similar to search anyway users = Obj.search(subquery_domain) user_values = [{ 'id': user.id, 'id_count': 1 } for user in users] else: user_values = Obj.read_group(subquery_domain, fields=[field_name], groupby=[field_name]) # user_values has format of read_group: [{'partner_id': 42, 'partner_id_count': 3},...] for goal in [g for g in goals if g.id in query_goals]: for user_value in user_values: queried_value = field_name in user_value and user_value[ field_name] or False if isinstance(queried_value, tuple) and len( queried_value) == 2 and isinstance( queried_value[0], pycompat.integer_types): queried_value = queried_value[0] if queried_value == query_goals[goal.id]: new_value = user_value.get( field_name + '_count', goal.current) goals_to_write.update( goal._get_write_values(new_value)) else: for goal in goals: # eval the domain with user replaced by goal user object domain = safe_eval(definition.domain, {'user': goal.user_id}) # add temporal clause(s) to the domain if fields are filled on the goal if goal.start_date and field_date_name: domain.append( (field_date_name, '>=', goal.start_date)) if goal.end_date and field_date_name: domain.append( (field_date_name, '<=', goal.end_date)) if definition.computation_mode == 'sum': field_name = definition.field_id.name # TODO for master: group on user field in batch mode res = Obj.read_group(domain, [field_name], []) new_value = res and res[0][field_name] or 0.0 else: # computation mode = count new_value = Obj.search_count(domain) goals_to_write.update( goal._get_write_values(new_value)) for goal, values in goals_to_write.items(): if not values: continue goal.write(values) if self.env.context.get('commit_gamification'): self.env.cr.commit() return True @api.multi def action_start(self): """Mark a goal as started. This should only be used when creating goals manually (in draft state)""" self.write({'state': 'inprogress'}) return self.update_goal() @api.multi def action_reach(self): """Mark a goal as reached. If the target goal condition is not met, the state will be reset to In Progress at the next goal update until the end date.""" return self.write({'state': 'reached'}) @api.multi def action_fail(self): """Set the state of the goal to failed. A failed goal will be ignored in future checks.""" return self.write({'state': 'failed'}) @api.multi def action_cancel(self): """Reset the completion after setting a goal as reached or failed. This is only the current state, if the date and/or target criteria match the conditions for a change of state, this will be applied at the next goal update.""" return self.write({'state': 'inprogress'}) @api.model def create(self, vals): return super(Goal, self.with_context(no_remind_goal=True)).create(vals) @api.multi def write(self, vals): """Overwrite the write method to update the last_update field to today If the current value is changed and the report frequency is set to On change, a report is generated """ vals['last_update'] = fields.Date.today() result = super(Goal, self).write(vals) for goal in self: if goal.state != "draft" and ('definition_id' in vals or 'user_id' in vals): # avoid drag&drop in kanban view raise exceptions.UserError( _('Can not modify the configuration of a started goal')) if vals.get( 'current') and 'no_remind_goal' not in self.env.context: if goal.challenge_id.report_message_frequency == 'onchange': goal.challenge_id.sudo().report_progress( users=goal.user_id) return result @api.multi def get_action(self): """Get the ir.action related to update the goal In case of a manual goal, should return a wizard to update the value :return: action description in a dictionary """ if self.definition_id.action_id: # open a the action linked to the goal action = self.definition_id.action_id.read()[0] if self.definition_id.res_id_field: current_user = self.env.user.sudo(self.env.user) action['res_id'] = safe_eval(self.definition_id.res_id_field, {'user': current_user}) # if one element to display, should see it in form mode if possible action['views'] = [(view_id, mode) for (view_id, mode) in action['views'] if mode == 'form'] or action['views'] return action if self.computation_mode == 'manually': # open a wizard window to update the value manually action = { 'name': _("Update %s") % self.definition_id.name, 'id': self.id, 'type': 'ir.actions.act_window', 'views': [[False, 'form']], 'target': 'new', 'context': { 'default_goal_id': self.id, 'default_current': self.current }, 'res_model': 'gamification.goal.wizard' } return action return False
class GoalDefinition(models.Model): """Goal definition A goal definition contains the way to evaluate an objective Each module wanting to be able to set goals to the users needs to create a new gamification_goal_definition """ _name = 'gamification.goal.definition' _description = 'Gamification goal definition' name = fields.Char("Goal Definition", required=True, translate=True) description = fields.Text("Goal Description") monetary = fields.Boolean( "Monetary Value", default=False, help="The target and current value are defined in the company currency." ) suffix = fields.Char("Suffix", help="The unit of the target and current values", translate=True) full_suffix = fields.Char("Full Suffix", compute='_compute_full_suffix', help="The currency and suffix field") computation_mode = fields.Selection( [ ('manually', "Recorded manually"), ('count', "Automatic: number of records"), ('sum', "Automatic: sum on a field"), ('python', "Automatic: execute a specific Python code"), ], default='manually', string="Computation Mode", required=True, help= "Defined how will be computed the goals. The result of the operation will be stored in the field 'Current'." ) display_mode = fields.Selection([ ('progress', "Progressive (using numerical values)"), ('boolean', "Exclusive (done or not-done)"), ], default='progress', string="Displayed as", required=True) model_id = fields.Many2one( 'ir.model', string='Model', help='The model object for the field to evaluate') field_id = fields.Many2one( 'ir.model.fields', string='Field to Sum', help='The field containing the value to evaluate') field_date_id = fields.Many2one( 'ir.model.fields', string='Date Field', help='The date to use for the time period evaluated') domain = fields.Char( "Filter Domain", required=True, default="[]", help="Domain for filtering records. General rule, not user depending," " e.g. [('state', '=', 'done')]. The expression can contain" " reference to 'user' which is a browse record of the current" " user if not in batch mode.") batch_mode = fields.Boolean( "Batch Mode", help="Evaluate the expression in batch instead of once for each user") batch_distinctive_field = fields.Many2one( 'ir.model.fields', string="Distinctive field for batch user", help= "In batch mode, this indicates which field distinct one user form the other, e.g. user_id, partner_id..." ) batch_user_expression = fields.Char( "Evaluated expression for batch mode", help= "The value to compare with the distinctive field. The expression can contain reference to 'user' which is a browse record of the current user, e.g. user.id, user.partner_id.id..." ) compute_code = fields.Text( "Python Code", help= "Python code to be executed for each user. 'result' should contains the new current value. Evaluated user can be access through object.user_id." ) condition = fields.Selection( [('higher', "The higher the better"), ('lower', "The lower the better")], default='higher', required=True, string="Goal Performance", help= "A goal is considered as completed when the current value is compared to the value to reach" ) action_id = fields.Many2one( 'ir.actions.act_window', string="Action", help="The action that will be called to update the goal value.") res_id_field = fields.Char( "ID Field of user", help= "The field name on the user profile (res.users) containing the value for res_id for action." ) @api.depends('suffix', 'monetary') # also depends of user... def _compute_full_suffix(self): for goal in self: items = [] if goal.monetary: items.append(self.env.user.company_id.currency_id.symbol or u'¤') if goal.suffix: items.append(goal.suffix) goal.full_suffix = u' '.join(items) def _check_domain_validity(self): # take admin as should always be present for definition in self: if definition.computation_mode not in ('count', 'sum'): continue Obj = self.env[definition.model_id.model] try: domain = safe_eval(definition.domain, {'user': self.env.user.sudo(self.env.user)}) # dummy search to make sure the domain is valid Obj.search_count(domain) except (ValueError, SyntaxError) as e: msg = e if isinstance(e, SyntaxError): msg = (e.msg + '\n' + e.text) raise exceptions.UserError( _("The domain for the definition %s seems incorrect, please check it.\n\n%s" ) % (definition.name, msg)) return True def _check_model_validity(self): """ make sure the selected field and model are usable""" for definition in self: try: if not (definition.model_id and definition.field_id): continue Model = self.env[definition.model_id.model] field = Model._fields.get(definition.field_id.name) if not (field and field.store): raise exceptions.UserError( _("The model configuration for the definition %s seems incorrect, please check it.\n\n%s not stored" ) % (definition.name, definition.field_id.name)) except KeyError as e: raise exceptions.UserError( _("The model configuration for the definition %s seems incorrect, please check it.\n\n%s not found" ) % (definition.name, e)) @api.model def create(self, vals): definition = super(GoalDefinition, self).create(vals) if definition.computation_mode in ('count', 'sum'): definition._check_domain_validity() if vals.get('field_id'): definition._check_model_validity() return definition @api.multi def write(self, vals): res = super(GoalDefinition, self).write(vals) if vals.get('computation_mode', 'count') in ('count', 'sum') and (vals.get('domain') or vals.get('model_id')): self._check_domain_validity() if vals.get('field_id') or vals.get('model_id') or vals.get( 'batch_mode'): self._check_model_validity() return res @api.onchange('model_id') def _change_model_id(self): """Force domain for the `field_id` and `field_date_id` fields""" if not self.model_id: return { 'domain': { 'field_id': expression.FALSE_DOMAIN, 'field_date_id': expression.FALSE_DOMAIN } } model_fields_domain = [('store', '=', True), '|', ('model_id', '=', self.model_id.id), ('model_id', 'in', self.model_id.inherited_model_ids.ids)] model_date_fields_domain = expression.AND([[ ('ttype', 'in', ('date', 'datetime')) ], model_fields_domain]) return { 'domain': { 'field_id': model_fields_domain, 'field_date_id': model_date_fields_domain } }
class Directory(dms_base.DMSModel): _name = 'muk_dms.directory' _description = "MuK Documents Directory" _inherit = 'muk_dms.access' _parent_store = True _parent_name = "parent_directory" _parent_order = 'parent_left' _order = 'parent_left' #---------------------------------------------------------- # Database #---------------------------------------------------------- name = fields.Char(string="Name", required=True) is_root_directory = fields.Boolean( string="Root Directory", default=False, help= "Indicates if the directory is a root directory. A root directory has a settings object, " + "while a directory with a set parent inherits the settings form its parent." ) settings = fields.Many2one('muk_dms.settings', string="Settings", store=True, auto_join=True, ondelete='restrict', compute='_compute_settings') parent_directory = fields.Many2one('muk_dms.directory', string="Parent Directory", ondelete='restrict', auto_join=True, index=True) child_directories = fields.One2many('muk_dms.directory', 'parent_directory', copy=False, string="Subdirectories") parent_left = fields.Integer(string='Left Parent', index=True) parent_right = fields.Integer(string='Right Parent', index=True) files = fields.One2many('muk_dms.file', 'directory', copy=False, string="Files") count_directories = fields.Integer(compute='_compute_count_directories', string="Subdirectories") count_files = fields.Integer(compute='_compute_count_files', string="Files") size = fields.Integer(compute='_compute_size', string="Size", store=True) custom_thumbnail = fields.Binary(string="Custom Thumbnail") thumbnail = fields.Binary(compute='_compute_thumbnail', string="Thumbnail") path = fields.Char(string="Path", store=True, compute='_compute_path') relational_path = fields.Text(string="Path", store=True, readonly=True, compute='_compute_relational_path') #---------------------------------------------------------- # Functions #---------------------------------------------------------- def lock_tree(self, user=None, refresh=False, operation=None, lock_self=True): if lock_self: self.lock(operation=operation) for record in self: record.child_directories.lock_tree(operation=operation) record.files.lock(operation=operation) def unlock_tree(self, refresh=False): self.unlock() for record in self: record.child_directories.unlock_tree() record.files.unlock() def notify_change(self, values, refresh=False, operation=None): super(Directory, self).notify_change(values, refresh) for child in self.child_directories: child.notify_change(values, refresh) for index, file in enumerate(self.files): if index == len(self.files) - 1: file.notify_change(values, refresh) else: file.notify_change(values, refresh) def trigger_computation_up(self, fields, operation=None): parent_directory = self.parent_directory if parent_directory: parent_directory.trigger_computation(fields, False) def trigger_computation_down(self, fields, operation=None): if self.settings.system_locks: self.lock_tree(operation=operation, lock_self=False) for child in self.child_directories: child.with_context(operation=operation, is_subnode=True).trigger_computation( fields, False, operation) for index, file in enumerate(self.files): if index == len(self.files) - 1: if self.env.context.get('is_subnode'): file.with_context(operation=operation).trigger_computation( fields, False, operation) else: file.with_context(operation=operation).trigger_computation( fields, True, operation) else: file.with_context(operation=operation).trigger_computation( fields, False, operation) self.unlock_tree() def trigger_computation(self, fields, refresh=True, operation=None): super(Directory, self).trigger_computation(fields, refresh, operation) values = {} if "settings" in fields: if not self.is_root_directory: values.update( self.with_context(operation=operation)._compute_settings( write=False)) if "path" in fields: values.update( self.with_context(operation=operation)._compute_path( write=False)) values.update( self.with_context( operation=operation)._compute_relational_path(write=False)) if "size" in fields: values.update( self.with_context(operation=operation)._compute_size( write=False)) if values: self.write(values) if "settings" in fields or "path" in fields: self.trigger_computation_down(fields, operation) if "size" in fields: self.trigger_computation_up(fields, operation) #---------------------------------------------------------- # Read, View #---------------------------------------------------------- def _compute_settings(self, write=True): if write: for record in self: if not record.is_root_directory: record.settings = record.parent_directory.settings else: self.ensure_one() return {'settings': self.parent_directory.settings.id} def _compute_path(self, write=True): def get_path(record): if record.is_root_directory: return "/%s/" % record.name else: return "%s%s/" % (record.parent_directory.path, record.name) if write: for record in self: record.path = get_path(record) else: self.ensure_one() return {'path': get_path(self)} def _compute_relational_path(self, write=True): def get_relational_path(record): if record.is_root_directory: return json.dumps([{ 'model': record._name, 'id': record.id, 'name': record.name }]) else: path = json.loads(record.parent_directory.relational_path) path.append({ 'model': record._name, 'id': record.id, 'name': record.name }) return json.dumps(path) if write: for record in self: record.relational_path = get_relational_path(record) else: self.ensure_one() return {'relational_path': get_relational_path(self)} def _compute_size(self, write=True): def get_size(record): size = 0 for directory in record.child_directories: size += directory.size for file in record.files: size += file.size return size if write: for record in self: record.size = get_size(record) else: self.ensure_one() return {'size': get_size(self)} @api.depends('child_directories') def _compute_count_directories(self): for record in self: record.count_directories = len(record.child_directories) @api.depends('files') def _compute_count_files(self): for record in self: record.count_files = len(record.files) @api.depends('custom_thumbnail') def _compute_thumbnail(self): for record in self: if record.custom_thumbnail: record.thumbnail = record.with_context({}).custom_thumbnail else: with open(os.path.join(_img_path, "folder.png"), "rb") as image_file: record.thumbnail = base64.b64encode(image_file.read()) #---------------------------------------------------------- # Create, Update, Delete #---------------------------------------------------------- @api.onchange('is_root_directory') def _onchange_directory_type(self): if self.is_root_directory: self.parent_directory = None else: self.settings = None def _before_create(self, vals): vals = super(Directory, self)._before_create(vals) is_root_directory = vals[ "is_root_directory"] if "is_root_directory" in vals else False if is_root_directory and not "settings" in vals: raise ValidationError( "A root directory has to have a settings object.") if not is_root_directory and not "parent_directory" in vals: raise ValidationError( "A directory has to have a parent directory.") return vals @api.constrains('settings', 'parent_directory') def _check_settings(self): if self.is_root_directory and not self.settings: raise ValidationError( "A root directory has to have a settings object.") if not self.is_root_directory and not self.parent_directory: raise ValidationError( "A directory has to have a parent directory.") @api.constrains('name') def _check_name(self): if not self.check_name(self.name): raise ValidationError("The directory name is invalid.") if self.is_root_directory: childs = self.sudo().settings.root_directories.mapped( lambda rec: [rec.id, rec.name]) else: childs = self.sudo().parent_directory.child_directories.mapped( lambda rec: [rec.id, rec.name]) duplicates = [ rec for rec in childs if rec[1] == self.name and rec[0] != self.id ] if duplicates: raise ValidationError( "A directory with the same name already exists.") def _after_create(self, vals): record = super(Directory, self)._after_create(vals) record._check_recomputation(vals) return record def _after_write_record(self, vals, operation): vals = super(Directory, self)._after_write_record(vals, operation) self._check_recomputation(vals, operation) return vals def _check_recomputation(self, values, operation=None): fields = [] if 'name' in values: fields.extend(['path']) if self.is_root_directory and 'settings' in values: fields.extend(['settings']) if not self.is_root_directory and 'parent_directory' in values: fields.extend(['settings', 'path']) if fields: self.trigger_computation(fields, operation=operation) @api.returns('self', lambda value: value.id) def copy(self, default=None): self.ensure_one() default = dict(default or []) names = [] if self.is_root_directory: names = self.sudo().settings.root_directories.mapped('name') default.update({'settings': self.settings.id}) elif 'parent_directory' in default: parent_directory = self.env['muk_dms.directory'].sudo().browse( default['parent_directory']) names = parent_directory.child_directories.mapped('name') else: names = self.sudo().parent_directory.child_directories.mapped( 'name') default.update({'name': self.unique_name(self.name, names)}) vals = self.copy_data(default)[0] new = self.with_context(lang=None).create(vals) self.copy_translations(new) for file in self.files: file.copy({'directory': new.id}) for directory in self.child_directories: directory.copy({'parent_directory': new.id}) return new def _before_unlink_record(self, operation): info = super(Directory, self)._before_unlink_record(operation) operation = self.env.context[ 'operation'] if 'operation' in self.env.context else operation if self.settings.system_locks and not 'operation' in self.env.context: info['lock_operation'] = operation self.lock_tree(operation=operation, lock_self=False) self.files.with_context(operation=operation).unlink() self.child_directories.with_context(operation=operation).unlink() return info def _after_unlink(self, result, info, infos, operation): super(Directory, self)._after_unlink(result, info, infos, operation) for info in infos: if 'lock_operation' in info: self.unlock_operation(info['lock_operation'])
class PushedFlow(models.Model): _name = "stock.location.path" _description = "Pushed Flow" _order = "sequence, name" name = fields.Char('Operation Name', required=True) company_id = fields.Many2one( 'res.company', 'Company', default=lambda self: self.env['res.company']._company_default_get('stock.location.path'), index=True) route_id = fields.Many2one('stock.location.route', 'Route', required=True, ondelete='cascade') location_from_id = fields.Many2one( 'stock.location', 'Source Location', index=True, ondelete='cascade', required=True, help="This rule can be applied when a move is confirmed that has this location as destination location") location_dest_id = fields.Many2one( 'stock.location', 'Destination Location', index=True, ondelete='cascade', required=True, help="The new location where the goods need to go") delay = fields.Integer('Delay (days)', default=0, help="Number of days needed to transfer the goods") picking_type_id = fields.Many2one( 'stock.picking.type', 'Operation Type', required=True, help="This is the operation type that will be put on the stock moves") auto = fields.Selection([ ('manual', 'Manual Operation'), ('transparent', 'Automatic No Step Added')], string='Automatic Move', default='manual', index=True, required=True, help="The 'Manual Operation' value will create a stock move after the current one. " "With 'Automatic No Step Added', the location is replaced in the original move.") propagate = fields.Boolean('Propagate cancel and split', default=True, help='If checked, when the previous move is cancelled or split, the move generated by this move will too') active = fields.Boolean('Active', default=True) warehouse_id = fields.Many2one('stock.warehouse', 'Warehouse') route_sequence = fields.Integer('Route Sequence', related='route_id.sequence', store=True) sequence = fields.Integer('Sequence') def _apply(self, move): new_date = (datetime.strptime(move.date_expected, DEFAULT_SERVER_DATETIME_FORMAT) + relativedelta.relativedelta(days=self.delay)).strftime(DEFAULT_SERVER_DATETIME_FORMAT) if self.auto == 'transparent': move.write({ 'date': new_date, 'date_expected': new_date, 'location_dest_id': self.location_dest_id.id}) # avoid looping if a push rule is not well configured; otherwise call again push_apply to see if a next step is defined if self.location_dest_id != move.location_dest_id: # TDE FIXME: should probably be done in the move model IMO move._push_apply() else: new_move_vals = self._prepare_move_copy_values(move, new_date) new_move = move.copy(new_move_vals) move.write({'move_dest_ids': [(4, new_move.id)]}) new_move._action_confirm() def _prepare_move_copy_values(self, move_to_copy, new_date): new_move_vals = { 'origin': move_to_copy.origin or move_to_copy.picking_id.name or "/", 'location_id': move_to_copy.location_dest_id.id, 'location_dest_id': self.location_dest_id.id, 'date': new_date, 'date_expected': new_date, 'company_id': self.company_id.id, 'picking_id': False, 'picking_type_id': self.picking_type_id.id, 'propagate': self.propagate, 'push_rule_id': self.id, 'warehouse_id': self.warehouse_id.id, } return new_move_vals
class LunchOrder(models.Model): """ A lunch order contains one or more lunch order line(s). It is associated to a user for a given date. When creating a lunch order, applicable lunch alerts are displayed. """ _name = 'lunch.order' _description = 'Lunch Order' _order = 'date desc' def _default_previous_order_ids(self): prev_order = self.env['lunch.order.line'].search( [('user_id', '=', self.env.uid), ('product_id.active', '!=', False)], limit=20, order='id desc') # If we return return prev_order.ids, we will have duplicates (identical orders). # Therefore, this following part removes duplicates based on product_id and note. return list({(order.product_id, order.note): order.id for order in prev_order}.values()) user_id = fields.Many2one('res.users', 'User', readonly=True, states={'new': [('readonly', False)]}, default=lambda self: self.env.uid) date = fields.Date('Date', required=True, readonly=True, states={'new': [('readonly', False)]}, default=fields.Date.context_today) order_line_ids = fields.One2many('lunch.order.line', 'order_id', 'Products', readonly=True, copy=True, states={ 'new': [('readonly', False)], False: [('readonly', False)] }) total = fields.Float(compute='_compute_total', string="Total", store=True) state = fields.Selection([('new', 'New'), ('confirmed', 'Received'), ('cancelled', 'Cancelled')], 'Status', readonly=True, index=True, copy=False, compute='_compute_order_state', store=True) alerts = fields.Text(compute='_compute_alerts_get', string="Alerts") company_id = fields.Many2one('res.company', related='user_id.company_id', store=True) currency_id = fields.Many2one('res.currency', related='company_id.currency_id', readonly=True, store=True) cash_move_balance = fields.Monetary(compute='_compute_cash_move_balance', multi='cash_move_balance') balance_visible = fields.Boolean(compute='_compute_cash_move_balance', multi='cash_move_balance') previous_order_ids = fields.Many2many('lunch.order.line', compute='_compute_previous_order') previous_order_widget = fields.Text(compute='_compute_previous_order') @api.one @api.depends('order_line_ids') def _compute_total(self): """ get and sum the order lines' price """ self.total = sum(orderline.price for orderline in self.order_line_ids) @api.multi def name_get(self): return [(order.id, '%s %s' % (_('Lunch Order'), '#%d' % order.id)) for order in self] @api.depends('state') def _compute_alerts_get(self): """ get the alerts to display on the order form """ alert_msg = [ alert.message for alert in self.env['lunch.alert'].search([]) if alert.display ] if self.state == 'new': self.alerts = alert_msg and '\n'.join(alert_msg) or False @api.multi @api.depends('user_id', 'state') def _compute_previous_order(self): self.ensure_one() self.previous_order_widget = json.dumps(False) prev_order = self.env['lunch.order.line'].search( [('user_id', '=', self.env.uid), ('product_id.active', '!=', False)], limit=20, order='date desc, id desc') # If we use prev_order.ids, we will have duplicates (identical orders). # Therefore, this following part removes duplicates based on product_id and note. self.previous_order_ids = list({(order.product_id, order.note): order.id for order in prev_order}.values()) if self.previous_order_ids: lunch_data = {} for line in self.previous_order_ids: lunch_data[line.id] = { 'line_id': line.id, 'product_id': line.product_id.id, 'product_name': line.product_id.name, 'supplier': line.supplier.name, 'note': line.note, 'price': line.price, 'date': line.date, 'currency_id': line.currency_id.id, } # sort the old lunch orders by (date, id) lunch_data = OrderedDict( sorted(lunch_data.items(), key=lambda t: (t[1]['date'], t[0]), reverse=True)) self.previous_order_widget = json.dumps(lunch_data) @api.one @api.depends('user_id') def _compute_cash_move_balance(self): domain = [('user_id', '=', self.user_id.id)] lunch_cash = self.env['lunch.cashmove'].read_group( domain, ['amount', 'user_id'], ['user_id']) if len(lunch_cash): self.cash_move_balance = lunch_cash[0]['amount'] self.balance_visible = (self.user_id == self.env.user) or self.user_has_groups( 'lunch.group_lunch_manager') @api.one @api.constrains('date') def _check_date(self): """ Prevents the user to create an order in the past """ date_order = datetime.datetime.strptime(self.date, '%Y-%m-%d') date_today = datetime.datetime.strptime( fields.Date.context_today(self), '%Y-%m-%d') if (date_order < date_today): raise ValidationError(_('The date of your order is in the past.')) @api.one @api.depends('order_line_ids.state') def _compute_order_state(self): """ Update the state of lunch.order based on its orderlines. Here is the logic: - if at least one order line is cancelled, the order is set as cancelled - if no line is cancelled but at least one line is not confirmed, the order is set as new - if all lines are confirmed, the order is set as confirmed """ if not self.order_line_ids: self.state = 'new' else: isConfirmed = True for orderline in self.order_line_ids: if orderline.state == 'cancelled': self.state = 'cancelled' return elif orderline.state == 'confirmed': continue else: isConfirmed = False if isConfirmed: self.state = 'confirmed' else: self.state = 'new' return
class Event(models.Model): _name = 'event.event' _inherit = ['event.event', 'website.seo.metadata', 'website.published.mixin'] website_published = fields.Boolean(track_visibility='onchange') website_id = fields.Many2one('website', string='Website', copy=False, default=lambda self: self.env.ref( 'website.default_website'), help='Website in which ' 'Event will published.') is_participating = fields.Boolean("Is Participating", compute="_compute_is_participating") website_menu = fields.Boolean( 'Dedicated Menu', compute='_compute_website_menu', inverse='_set_website_menu', help="Creates menus Introduction, Location and Register on the page " " of the event on the website.", store=True) menu_id = fields.Many2one('website.menu', 'Event Menu', copy=False) def _compute_is_participating(self): # we don't allow public user to see participating label if self.env.user != self.env.ref('base.public_user'): email = self.env.user.partner_id.email for event in self: domain = ['&', '|', ('email', '=', email), ('partner_id', '=', self.env.user.partner_id.id), ('event_id', '=', event.id)] event.is_participating = self.env['event.registration'].search_count(domain) @api.multi @api.depends('name') def _compute_website_url(self): super(Event, self)._compute_website_url() for event in self: if event.id: # avoid to perform a slug on a not yet saved record in case of an onchange. event.website_url = '/event/%s' % slug(event) @api.onchange('event_type_id') def _onchange_type(self): super(Event, self)._onchange_type() if self.event_type_id: self.website_menu = self.event_type_id.website_menu def _get_standard_menu_entries_names(self): """ Method returning menu entries possibly generated by event modules. It is used to distinguish module-generated pages from custom pages. """ return [_('Introduction'), _('Location'), _('Register')] def _get_menu_entries(self): """ Method returning menu entries to display on the website view of the event, possibly depending on some options in inheriting modules. """ self.ensure_one() return [ (_('Introduction'), False, 'website_event.template_intro'), (_('Location'), False, 'website_event.template_location'), (_('Register'), '/event/%s/register' % slug(self), False), ] @api.multi def _set_website_menu(self): for event in self: if event.menu_id and not event.website_menu: event.menu_id.unlink() elif event.website_menu: if not event.menu_id: root_menu = self.env['website.menu'].create({'name': event.name}) event.menu_id = root_menu existing_page_names = event.menu_id.child_id.mapped('name') required_page_names = [entry[0] for entry in event._get_menu_entries()] standard_page_names = self._get_standard_menu_entries_names() # remove entries that should not exist anymore submenu_to_delete = event.menu_id.child_id.filtered(lambda menu: menu.name not in required_page_names and menu.name in standard_page_names) submenu_to_delete.unlink() # create missing entries for sequence, (name, url, xml_id) in enumerate(event._get_menu_entries()): if name not in existing_page_names: if not url: newpath = self.env['website'].new_page(name + ' ' + event.name, template=xml_id, ispage=False)['url'] url = "/event/" + slug(event) + "/page/" + newpath[1:] self.env['website.menu'].create({ 'name': name, 'url': url, 'parent_id': event.menu_id.id, 'sequence': sequence, }) @api.multi def _compute_website_menu(self): for event in self: event.website_menu = bool(event.menu_id) @api.multi def google_map_img(self, zoom=8, width=298, height=298): self.ensure_one() if self.address_id: return self.sudo().address_id.google_map_img(zoom=zoom, width=width, height=height) return None @api.multi def google_map_link(self, zoom=8): self.ensure_one() if self.address_id: return self.sudo().address_id.google_map_link(zoom=zoom) return None @api.multi def _track_subtype(self, init_values): self.ensure_one() if 'website_published' in init_values and self.website_published: return 'website_event.mt_event_published' elif 'website_published' in init_values and not self.website_published: return 'website_event.mt_event_unpublished' return super(Event, self)._track_subtype(init_values) @api.multi def action_open_badge_editor(self): """ open the event badge editor : redirect to the report page of event badge report """ self.ensure_one() return { 'type': 'ir.actions.act_url', 'target': 'new', 'url': '/report/html/%s/%s?enable_editor' % ('event.event_event_report_template_badge', self.id), }
class LunchOrderLine(models.Model): _name = 'lunch.order.line' _description = 'lunch order line' _order = 'date desc, id desc' name = fields.Char(related='product_id.name', string="Product Name", readonly=True) order_id = fields.Many2one('lunch.order', 'Order', ondelete='cascade', required=True) product_id = fields.Many2one('lunch.product', 'Product', required=True) category_id = fields.Many2one('lunch.product.category', string='Product Category', related='product_id.category_id', readonly=True, store=True) date = fields.Date(string='Date', related='order_id.date', readonly=True, store=True) supplier = fields.Many2one('res.partner', string='Vendor', related='product_id.supplier', readonly=True, store=True) user_id = fields.Many2one('res.users', string='User', related='order_id.user_id', readonly=True, store=True) note = fields.Text('Note') price = fields.Float(related='product_id.price', readonly=True, store=True, digits=dp.get_precision('Account')) state = fields.Selection([('new', 'New'), ('confirmed', 'Received'), ('ordered', 'Ordered'), ('cancelled', 'Cancelled')], 'Status', readonly=True, index=True, default='new') cashmove = fields.One2many('lunch.cashmove', 'order_id', 'Cash Move') currency_id = fields.Many2one('res.currency', related='order_id.currency_id') @api.one def order(self): """ The order_line is ordered to the vendor but isn't received yet """ if self.user_has_groups("lunch.group_lunch_manager"): self.state = 'ordered' else: raise AccessError( _("Only your lunch manager processes the orders.")) @api.one def confirm(self): """ confirm one or more order line, update order status and create new cashmove """ if self.user_has_groups("lunch.group_lunch_manager"): if self.state != 'confirmed': values = { 'user_id': self.user_id.id, 'amount': -self.price, 'description': self.product_id.name, 'order_id': self.id, 'state': 'order', 'date': self.date, } self.env['lunch.cashmove'].create(values) self.state = 'confirmed' else: raise AccessError( _("Only your lunch manager sets the orders as received.")) @api.one def cancel(self): """ cancel one or more order.line, update order status and unlink existing cashmoves """ if self.user_has_groups("lunch.group_lunch_manager"): self.state = 'cancelled' self.cashmove.unlink() else: raise AccessError(_("Only your lunch manager cancels the orders."))
class OpFacilityLine(models.Model): _inherit = 'op.facility.line' classroom_id = fields.Many2one('op.classroom', 'Classroom')
class AccountFiscalPosition(models.Model): _name = 'account.fiscal.position' _description = 'Fiscal Position' _order = 'sequence' sequence = fields.Integer() name = fields.Char(string='Fiscal Position', required=True) active = fields.Boolean( default=True, help= "By unchecking the active field, you may hide a fiscal position without deleting it." ) company_id = fields.Many2one('res.company', string='Company') account_ids = fields.One2many('account.fiscal.position.account', 'position_id', string='Account Mapping', copy=True) tax_ids = fields.One2many('account.fiscal.position.tax', 'position_id', string='Tax Mapping', copy=True) note = fields.Text( 'Notes', translate=True, help="Legal mentions that have to be printed on the invoices.") auto_apply = fields.Boolean( string='Detect Automatically', help="Apply automatically this fiscal position.") vat_required = fields.Boolean( string='VAT required', help="Apply only if partner has a VAT number.") country_id = fields.Many2one( 'res.country', string='Country', help="Apply only if delivery or invoicing country match.") country_group_id = fields.Many2one( 'res.country.group', string='Country Group', help="Apply only if delivery or invocing country match the group.") state_ids = fields.Many2many('res.country.state', string='Federal States') zip_from = fields.Integer(string='Zip Range From', default=0) zip_to = fields.Integer(string='Zip Range To', default=0) # To be used in hiding the 'Federal States' field('attrs' in view side) when selected 'Country' has 0 states. states_count = fields.Integer(compute='_compute_states_count') @api.one def _compute_states_count(self): self.states_count = len(self.country_id.state_ids) @api.one @api.constrains('zip_from', 'zip_to') def _check_zip(self): if self.zip_from > self.zip_to: raise ValidationError( _('Invalid "Zip Range", please configure it properly.')) return True @api.model # noqa def map_tax(self, taxes, product=None, partner=None): result = self.env['account.tax'].browse() for tax in taxes: tax_count = 0 for t in self.tax_ids: if t.tax_src_id == tax: tax_count += 1 if t.tax_dest_id: result |= t.tax_dest_id if not tax_count: result |= tax return result @api.model def map_account(self, account): for pos in self.account_ids: if pos.account_src_id == account: return pos.account_dest_id return account @api.model def map_accounts(self, accounts): """ Receive a dictionary having accounts in values and try to replace those accounts accordingly to the fiscal position. """ ref_dict = {} for line in self.account_ids: ref_dict[line.account_src_id] = line.account_dest_id for key, acc in accounts.items(): if acc in ref_dict: accounts[key] = ref_dict[acc] return accounts @api.onchange('country_id') def _onchange_country_id(self): if self.country_id: self.zip_from = self.zip_to = self.country_group_id = False self.state_ids = [(5, )] self.states_count = len(self.country_id.state_ids) @api.onchange('country_group_id') def _onchange_country_group_id(self): if self.country_group_id: self.zip_from = self.zip_to = self.country_id = False self.state_ids = [(5, )] @api.model def _get_fpos_by_region(self, country_id=False, state_id=False, zipcode=False, vat_required=False): if not country_id: return False base_domain = [('auto_apply', '=', True), ('vat_required', '=', vat_required)] if self.env.context.get('force_company'): base_domain.append( ('company_id', '=', self.env.context.get('force_company'))) null_state_dom = state_domain = [('state_ids', '=', False)] null_zip_dom = zip_domain = [('zip_from', '=', 0), ('zip_to', '=', 0)] null_country_dom = [('country_id', '=', False), ('country_group_id', '=', False)] if zipcode and zipcode.isdigit(): zipcode = int(zipcode) zip_domain = [('zip_from', '<=', zipcode), ('zip_to', '>=', zipcode)] else: zipcode = 0 if state_id: state_domain = [('state_ids', '=', state_id)] domain_country = base_domain + [('country_id', '=', country_id)] domain_group = base_domain + [ ('country_group_id.country_ids', '=', country_id) ] # Build domain to search records with exact matching criteria fpos = self.search(domain_country + state_domain + zip_domain, limit=1) # return records that fit the most the criteria, and fallback on less specific fiscal positions if any can be found if not fpos and state_id: fpos = self.search(domain_country + null_state_dom + zip_domain, limit=1) if not fpos and zipcode: fpos = self.search(domain_country + state_domain + null_zip_dom, limit=1) if not fpos and state_id and zipcode: fpos = self.search(domain_country + null_state_dom + null_zip_dom, limit=1) # fallback: country group with no state/zip range if not fpos: fpos = self.search(domain_group + null_state_dom + null_zip_dom, limit=1) if not fpos: # Fallback on catchall (no country, no group) fpos = self.search(base_domain + null_country_dom, limit=1) return fpos or False @api.model def get_fiscal_position(self, partner_id, delivery_id=None): if not partner_id: return False # This can be easily overriden to apply more complex fiscal rules PartnerObj = self.env['res.partner'] partner = PartnerObj.browse(partner_id) # if no delivery use invoicing if delivery_id: delivery = PartnerObj.browse(delivery_id) else: delivery = partner # partner manually set fiscal position always win if delivery.property_account_position_id or partner.property_account_position_id: return delivery.property_account_position_id.id or partner.property_account_position_id.id # First search only matching VAT positions vat_required = bool(partner.vat) fp = self._get_fpos_by_region(delivery.country_id.id, delivery.state_id.id, delivery.zip, vat_required) # Then if VAT required found no match, try positions that do not require it if not fp and vat_required: fp = self._get_fpos_by_region(delivery.country_id.id, delivery.state_id.id, delivery.zip, False) return fp.id if fp else False
class ResPartnerBank(models.Model): _name = 'res.partner.bank' _rec_name = 'acc_number' _description = 'Bank Accounts' _order = 'sequence, id' @api.model def get_supported_account_types(self): return self._get_supported_account_types() @api.model def _get_supported_account_types(self): return [('bank', _('Normal'))] active = fields.Boolean(default=True) acc_type = fields.Selection( selection=lambda x: x.env['res.partner.bank' ].get_supported_account_types(), compute='_compute_acc_type', string='Type', help= 'Bank account type: Normal or IBAN. Inferred from the bank account number.' ) acc_number = fields.Char('Account Number', required=True) sanitized_acc_number = fields.Char(compute='_compute_sanitized_acc_number', string='Sanitized Account Number', readonly=True, store=True) acc_holder_name = fields.Char( string='Account Holder Name', help= "Account holder name, in case it is different than the name of the Account Holder" ) partner_id = fields.Many2one( 'res.partner', 'Account Holder', ondelete='cascade', index=True, domain=['|', ('is_company', '=', True), ('parent_id', '=', False)], required=True) bank_id = fields.Many2one('res.bank', string='Bank') bank_name = fields.Char(related='bank_id.name', readonly=False) bank_bic = fields.Char(related='bank_id.bic', readonly=False) sequence = fields.Integer(default=10) currency_id = fields.Many2one('res.currency', string='Currency') company_id = fields.Many2one('res.company', 'Company', default=lambda self: self.env.company, ondelete='cascade', readonly=True) _sql_constraints = [ ('unique_number', 'unique(sanitized_acc_number, company_id)', 'Account Number must be unique'), ] @api.depends('acc_number') def _compute_sanitized_acc_number(self): for bank in self: bank.sanitized_acc_number = sanitize_account_number( bank.acc_number) @api.depends('acc_number') def _compute_acc_type(self): for bank in self: bank.acc_type = self.retrieve_acc_type(bank.acc_number) @api.model def retrieve_acc_type(self, acc_number): """ To be overridden by subclasses in order to support other account_types. """ return 'bank' @api.model def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None): pos = 0 while pos < len(args): # DLE P14 if args[pos][0] == 'acc_number': op = args[pos][1] value = args[pos][2] if not isinstance(value, str) and isinstance(value, Iterable): value = [sanitize_account_number(i) for i in value] else: value = sanitize_account_number(value) if 'like' in op: value = '%' + value + '%' args[pos] = ('sanitized_acc_number', op, value) pos += 1 return super(ResPartnerBank, self)._search(args, offset, limit, order, count=count, access_rights_uid=access_rights_uid)
class ResPartner(models.Model): _name = 'res.partner' _inherit = 'res.partner' @api.multi def _credit_debit_get(self): tables, where_clause, where_params = self.env[ 'account.move.line']._query_get() where_params = [tuple(self.ids)] + where_params if where_clause: where_clause = 'AND ' + where_clause self._cr.execute( """SELECT account_move_line.partner_id, act.type, SUM(account_move_line.amount_residual) FROM account_move_line LEFT JOIN account_account a ON (account_move_line.account_id=a.id) LEFT JOIN account_account_type act ON (a.user_type_id=act.id) WHERE act.type IN ('receivable','payable') AND account_move_line.partner_id IN %s AND account_move_line.reconciled IS FALSE """ + where_clause + """ GROUP BY account_move_line.partner_id, act.type """, where_params) for pid, type, val in self._cr.fetchall(): partner = self.browse(pid) if type == 'receivable': partner.credit = val elif type == 'payable': partner.debit = -val @api.multi def _asset_difference_search(self, account_type, operator, operand): if operator not in ('<', '=', '>', '>=', '<='): return [] if type(operand) not in (float, int): return [] sign = 1 if account_type == 'payable': sign = -1 res = self._cr.execute( ''' SELECT partner.id FROM res_partner partner LEFT JOIN account_move_line aml ON aml.partner_id = partner.id RIGHT JOIN account_account acc ON aml.account_id = acc.id WHERE acc.internal_type = %s AND NOT acc.deprecated GROUP BY partner.id HAVING %s * COALESCE(SUM(aml.amount_residual), 0) ''' + operator + ''' %s''', (account_type, sign, operand)) res = self._cr.fetchall() if not res: return [('id', '=', '0')] return [('id', 'in', [r[0] for r in res])] @api.model def _credit_search(self, operator, operand): return self._asset_difference_search('receivable', operator, operand) @api.model def _debit_search(self, operator, operand): return self._asset_difference_search('payable', operator, operand) @api.multi def _invoice_total(self): account_invoice_report = self.env['account.invoice.report'] if not self.ids: self.total_invoiced = 0.0 return True user_currency_id = self.env.user.company_id.currency_id.id all_partners_and_children = {} all_partner_ids = [] for partner in self: # price_total is in the company currency all_partners_and_children[partner] = self.with_context( active_test=False).search([('id', 'child_of', partner.id)]).ids all_partner_ids += all_partners_and_children[partner] # searching account.invoice.report via the orm is comparatively expensive # (generates queries "id in []" forcing to build the full table). # In simple cases where all invoices are in the same currency than the user's company # access directly these elements # generate where clause to include multicompany rules where_query = account_invoice_report._where_calc([ ('partner_id', 'in', all_partner_ids), ('state', 'not in', ['draft', 'cancel']), ('type', 'in', ('out_invoice', 'out_refund')) ]) account_invoice_report._apply_ir_rules(where_query, 'read') from_clause, where_clause, where_clause_params = where_query.get_sql() # price_total is in the company currency query = """ SELECT SUM(price_total) as total, partner_id FROM account_invoice_report account_invoice_report WHERE %s GROUP BY partner_id """ % where_clause self.env.cr.execute(query, where_clause_params) price_totals = self.env.cr.dictfetchall() for partner, child_ids in all_partners_and_children.items(): partner.total_invoiced = sum(price['total'] for price in price_totals if price['partner_id'] in child_ids) @api.multi def _compute_journal_item_count(self): AccountMoveLine = self.env['account.move.line'] for partner in self: partner.journal_item_count = AccountMoveLine.search_count([ ('partner_id', '=', partner.id) ]) @api.multi def _compute_contracts_count(self): AccountAnalyticAccount = self.env['account.analytic.account'] for partner in self: partner.contracts_count = AccountAnalyticAccount.search_count([ ('partner_id', '=', partner.id) ]) def get_followup_lines_domain(self, date, overdue_only=False, only_unblocked=False): domain = [('reconciled', '=', False), ('account_id.deprecated', '=', False), ('account_id.internal_type', '=', 'receivable'), '|', ('debit', '!=', 0), ('credit', '!=', 0), ('company_id', '=', self.env.user.company_id.id)] if only_unblocked: domain += [('blocked', '=', False)] if self.ids: if 'exclude_given_ids' in self._context: domain += [('partner_id', 'not in', self.ids)] else: domain += [('partner_id', 'in', self.ids)] #adding the overdue lines overdue_domain = [ '|', '&', ('date_maturity', '!=', False), ('date_maturity', '<', date), '&', ('date_maturity', '=', False), ('date', '<', date) ] if overdue_only: domain += overdue_domain return domain @api.one def _compute_has_unreconciled_entries(self): # Avoid useless work if has_unreconciled_entries is not relevant for this partner if not self.active or not self.is_company and self.parent_id: return self.env.cr.execute( """ SELECT 1 FROM( SELECT p.last_time_entries_checked AS last_time_entries_checked, MAX(l.write_date) AS max_date FROM account_move_line l RIGHT JOIN account_account a ON (a.id = l.account_id) RIGHT JOIN res_partner p ON (l.partner_id = p.id) WHERE p.id = %s AND EXISTS ( SELECT 1 FROM account_move_line l WHERE l.account_id = a.id AND l.partner_id = p.id AND l.amount_residual > 0 ) AND EXISTS ( SELECT 1 FROM account_move_line l WHERE l.account_id = a.id AND l.partner_id = p.id AND l.amount_residual < 0 ) GROUP BY p.last_time_entries_checked ) as s WHERE (last_time_entries_checked IS NULL OR max_date > last_time_entries_checked) """, (self.id, )) self.has_unreconciled_entries = self.env.cr.rowcount == 1 @api.multi def mark_as_reconciled(self): self.env['account.partial.reconcile'].check_access_rights('write') return self.sudo().with_context( company_id=self.env.user.company_id.id).write({ 'last_time_entries_checked': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT) }) @api.one def _get_company_currency(self): if self.company_id: self.currency_id = self.sudo().company_id.currency_id else: self.currency_id = self.env.user.company_id.currency_id credit = fields.Monetary(compute='_credit_debit_get', search=_credit_search, string='Total Receivable', help="Total amount this customer owes you.") debit = fields.Monetary( compute='_credit_debit_get', search=_debit_search, string='Total Payable', help="Total amount you have to pay to this vendor.") debit_limit = fields.Monetary('Payable Limit') total_invoiced = fields.Monetary(compute='_invoice_total', string="Total Invoiced", groups='account.group_account_invoice') currency_id = fields.Many2one( 'res.currency', compute='_get_company_currency', readonly=True, string="Currency", help='Utility field to express amount currency') contracts_count = fields.Integer(compute='_compute_contracts_count', string="Contracts", type='integer') journal_item_count = fields.Integer(compute='_compute_journal_item_count', string="Journal Items", type="integer") property_account_payable_id = fields.Many2one( 'account.account', company_dependent=True, string="Account Payable", oldname="property_account_payable", domain= "[('internal_type', '=', 'payable'), ('deprecated', '=', False)]", help= "This account will be used instead of the default one as the payable account for the current partner", required=True) property_account_receivable_id = fields.Many2one( 'account.account', company_dependent=True, string="Account Receivable", oldname="property_account_receivable", domain= "[('internal_type', '=', 'receivable'), ('deprecated', '=', False)]", help= "This account will be used instead of the default one as the receivable account for the current partner", required=True) property_account_position_id = fields.Many2one( 'account.fiscal.position', company_dependent=True, string="Fiscal Position", help= "The fiscal position will determine taxes and accounts used for the partner.", oldname="property_account_position") property_payment_term_id = fields.Many2one( 'account.payment.term', company_dependent=True, string='Customer Payment Terms', help= "This payment term will be used instead of the default one for sales orders and customer invoices", oldname="property_payment_term") property_supplier_payment_term_id = fields.Many2one( 'account.payment.term', company_dependent=True, string='Vendor Payment Terms', help= "This payment term will be used instead of the default one for purchase orders and vendor bills", oldname="property_supplier_payment_term") ref_company_ids = fields.One2many( 'res.company', 'partner_id', string='Companies that refers to partner', oldname="ref_companies") has_unreconciled_entries = fields.Boolean( compute='_compute_has_unreconciled_entries', help= "The partner has at least one unreconciled debit and credit since last time the invoices & payments matching was performed." ) last_time_entries_checked = fields.Datetime( oldname='last_reconciliation_date', string='Latest Invoices & Payments Matching Date', readonly=True, copy=False, help= 'Last time the invoices & payments matching was performed for this partner. ' 'It is set either if there\'s not at least an unreconciled debit and an unreconciled credit ' 'or if you click the "Done" button.') invoice_ids = fields.One2many('account.invoice', 'partner_id', string='Invoices', readonly=True, copy=False) contract_ids = fields.One2many('account.analytic.account', 'partner_id', string='Contracts', readonly=True) bank_account_count = fields.Integer(compute='_compute_bank_count', string="Bank") trust = fields.Selection([('good', 'Good Debtor'), ('normal', 'Normal Debtor'), ('bad', 'Bad Debtor')], string='Degree of trust you have in this debtor', default='normal', company_dependent=True) invoice_warn = fields.Selection(WARNING_MESSAGE, 'Invoice', help=WARNING_HELP, required=True, default="no-message") invoice_warn_msg = fields.Text('Message for Invoice') @api.multi def _compute_bank_count(self): bank_data = self.env['res.partner.bank'].read_group( [('partner_id', 'in', self.ids)], ['partner_id'], ['partner_id']) mapped_data = dict([(bank['partner_id'][0], bank['partner_id_count']) for bank in bank_data]) for partner in self: partner.bank_account_count = mapped_data.get(partner.id, 0) def _find_accounting_partner(self, partner): ''' Find the partner for which the accounting entries will be created ''' return partner.commercial_partner_id @api.model def _commercial_fields(self): return super(ResPartner, self)._commercial_fields() + \ ['debit_limit', 'property_account_payable_id', 'property_account_receivable_id', 'property_account_position_id', 'property_payment_term_id', 'property_supplier_payment_term_id', 'last_time_entries_checked'] @api.multi def action_view_partner_invoices(self): self.ensure_one() action = self.env.ref( 'account.action_invoice_refund_out_tree').read()[0] action['domain'] = literal_eval(action['domain']) action['domain'].append(('partner_id', 'child_of', self.id)) return action
class WebsiteVisitor(models.Model): _name = 'website.visitor' _description = 'Website Visitor' _order = 'last_connection_datetime DESC' name = fields.Char('Name') access_token = fields.Char(required=True, default=lambda x: uuid.uuid4().hex, index=False, copy=False, groups='base.group_website_publisher') active = fields.Boolean('Active', default=True) website_id = fields.Many2one('website', "Website", readonly=True) partner_id = fields.Many2one('res.partner', string="Linked Partner", help="Partner of the last logged in user.") partner_image = fields.Binary(related='partner_id.image_1920') # localisation and info country_id = fields.Many2one('res.country', 'Country', readonly=True) country_flag = fields.Char(related="country_id.image_url", string="Country Flag") lang_id = fields.Many2one( 'res.lang', string='Language', help="Language from the website when visitor has been created") timezone = fields.Selection(_tz_get, string='Timezone') email = fields.Char(string='Email', compute='_compute_email_phone') mobile = fields.Char(string='Mobile Phone', compute='_compute_email_phone') # Visit fields visit_count = fields.Integer( 'Number of visits', default=1, readonly=True, help= "A new visit is considered if last connection was more than 8 hours ago." ) website_track_ids = fields.One2many('website.track', 'visitor_id', string='Visited Pages History', readonly=True) visitor_page_count = fields.Integer( 'Page Views', compute="_compute_page_statistics", help="Total number of visits on tracked pages") page_ids = fields.Many2many('website.page', string="Visited Pages", compute="_compute_page_statistics", groups="website.group_website_designer") page_count = fields.Integer('# Visited Pages', compute="_compute_page_statistics", help="Total number of tracked page visited") last_visited_page_id = fields.Many2one( 'website.page', string="Last Visited Page", compute="_compute_last_visited_page_id") # Time fields create_date = fields.Datetime('First connection date', readonly=True) last_connection_datetime = fields.Datetime('Last Connection', default=fields.Datetime.now, help="Last page view date", readonly=True) time_since_last_action = fields.Char( 'Last action', compute="_compute_time_statistics", help='Time since last page view. E.g.: 2 minutes ago') is_connected = fields.Boolean( 'Is connected ?', compute='_compute_time_statistics', help= 'A visitor is considered as connected if his last page view was within the last 5 minutes.' ) _sql_constraints = [ ('access_token_unique', 'unique(access_token)', 'Access token should be unique.'), ('partner_uniq', 'unique(partner_id)', 'A partner is linked to only one visitor.'), ] @api.depends('name') def name_get(self): return [(record.id, (record.name or _('Website Visitor #%s', record.id))) for record in self] @api.depends('partner_id.email_normalized', 'partner_id.mobile', 'partner_id.phone') def _compute_email_phone(self): results = self.env['res.partner'].search_read( [('id', 'in', self.partner_id.ids)], ['id', 'email_normalized', 'mobile', 'phone'], ) mapped_data = { result['id']: { 'email_normalized': result['email_normalized'], 'mobile': result['mobile'] if result['mobile'] else result['phone'] } for result in results } for visitor in self: visitor.email = mapped_data.get(visitor.partner_id.id, {}).get('email_normalized') visitor.mobile = mapped_data.get(visitor.partner_id.id, {}).get('mobile') @api.depends('website_track_ids') def _compute_page_statistics(self): results = self.env['website.track'].read_group( [('visitor_id', 'in', self.ids), ('url', '!=', False)], ['visitor_id', 'page_id', 'url'], ['visitor_id', 'page_id', 'url'], lazy=False) mapped_data = {} for result in results: visitor_info = mapped_data.get(result['visitor_id'][0], { 'page_count': 0, 'visitor_page_count': 0, 'page_ids': set() }) visitor_info['visitor_page_count'] += result['__count'] visitor_info['page_count'] += 1 if result['page_id']: visitor_info['page_ids'].add(result['page_id'][0]) mapped_data[result['visitor_id'][0]] = visitor_info for visitor in self: visitor_info = mapped_data.get(visitor.id, { 'page_count': 0, 'visitor_page_count': 0, 'page_ids': set() }) visitor.page_ids = [(6, 0, visitor_info['page_ids'])] visitor.visitor_page_count = visitor_info['visitor_page_count'] visitor.page_count = visitor_info['page_count'] @api.depends('website_track_ids.page_id') def _compute_last_visited_page_id(self): results = self.env['website.track'].read_group( [('visitor_id', 'in', self.ids)], ['visitor_id', 'page_id', 'visit_datetime:max'], ['visitor_id', 'page_id'], lazy=False) mapped_data = { result['visitor_id'][0]: result['page_id'][0] for result in results if result['page_id'] } for visitor in self: visitor.last_visited_page_id = mapped_data.get(visitor.id, False) @api.depends('last_connection_datetime') def _compute_time_statistics(self): for visitor in self: visitor.time_since_last_action = _format_time_ago( self.env, (datetime.now() - visitor.last_connection_datetime)) visitor.is_connected = ( datetime.now() - visitor.last_connection_datetime) < timedelta(minutes=5) def _check_for_message_composer(self): """ Purpose of this method is to actualize visitor model prior to contacting him. Used notably for inheritance purpose, when dealing with leads that could update the visitor model. """ return bool(self.partner_id and self.partner_id.email) def _prepare_message_composer_context(self): return { 'default_model': 'res.partner', 'default_res_id': self.partner_id.id, 'default_partner_ids': [self.partner_id.id], } def action_send_mail(self): self.ensure_one() if not self._check_for_message_composer(): raise UserError( _("There are no contact and/or no email linked to this visitor." )) visitor_composer_ctx = self._prepare_message_composer_context() compose_form = self.env.ref('mail.email_compose_message_wizard_form', False) compose_ctx = dict( default_use_template=False, default_composition_mode='comment', ) compose_ctx.update(**visitor_composer_ctx) return { 'name': _('Contact Visitor'), 'type': 'ir.actions.act_window', 'view_mode': 'form', 'res_model': 'mail.compose.message', 'views': [(compose_form.id, 'form')], 'view_id': compose_form.id, 'target': 'new', 'context': compose_ctx, } def _get_visitor_from_request(self, force_create=False): """ Return the visitor as sudo from the request if there is a visitor_uuid cookie. It is possible that the partner has changed or has disconnected. In that case the cookie is still referencing the old visitor and need to be replaced with the one of the visitor returned !!!. """ # This function can be called in json with mobile app. # In case of mobile app, no uid is set on the jsonRequest env. # In case of multi db, _env is None on request, and request.env unbound. if not request: return None Visitor = self.env['website.visitor'].sudo() visitor = Visitor access_token = request.httprequest.cookies.get('visitor_uuid') if access_token: visitor = Visitor.with_context(active_test=False).search([ ('access_token', '=', access_token) ]) # Prefetch access_token and other fields. Since access_token has a restricted group and we access # a non restricted field (partner_id) first it is not fetched and will require an additional query to be retrieved. visitor.access_token if not self.env.user._is_public(): partner_id = self.env.user.partner_id if not visitor or visitor.partner_id and visitor.partner_id != partner_id: # Partner and no cookie or wrong cookie visitor = Visitor.with_context(active_test=False).search([ ('partner_id', '=', partner_id.id) ]) elif visitor and visitor.partner_id: # Cookie associated to a Partner visitor = Visitor if visitor and not visitor.timezone: tz = self._get_visitor_timezone() if tz: visitor.timezone = tz if not visitor and force_create: visitor = self._create_visitor() return visitor def _handle_webpage_dispatch(self, response, website_page): # get visitor. Done here to avoid having to do it multiple times in case of override. visitor_sudo = self._get_visitor_from_request(force_create=True) if request.httprequest.cookies.get('visitor_uuid', '') != visitor_sudo.access_token: expiration_date = datetime.now() + timedelta(days=365) response.set_cookie('visitor_uuid', visitor_sudo.access_token, expires=expiration_date) self._handle_website_page_visit(website_page, visitor_sudo) def _handle_website_page_visit(self, website_page, visitor_sudo): """ Called on dispatch. This will create a website.visitor if the http request object is a tracked website page or a tracked view. Only on tracked elements to avoid having too much operations done on every page or other http requests. Note: The side effect is that the last_connection_datetime is updated ONLY on tracked elements.""" url = request.httprequest.url website_track_values = { 'url': url, 'visit_datetime': datetime.now(), } if website_page: website_track_values['page_id'] = website_page.id domain = [('page_id', '=', website_page.id)] else: domain = [('url', '=', url)] visitor_sudo._add_tracking(domain, website_track_values) if visitor_sudo.lang_id.id != request.lang.id: visitor_sudo.write({'lang_id': request.lang.id}) def _add_tracking(self, domain, website_track_values): """ Add the track and update the visitor""" domain = expression.AND([domain, [('visitor_id', '=', self.id)]]) last_view = self.env['website.track'].sudo().search(domain, limit=1) if not last_view or last_view.visit_datetime < datetime.now( ) - timedelta(minutes=30): website_track_values['visitor_id'] = self.id self.env['website.track'].create(website_track_values) self._update_visitor_last_visit() def _create_visitor(self): """ Create a visitor. Tracking is added after the visitor has been created.""" country_code = request.session.get('geoip', {}).get('country_code', False) country_id = request.env['res.country'].sudo().search( [('code', '=', country_code)], limit=1).id if country_code else False vals = { 'lang_id': request.lang.id, 'country_id': country_id, 'website_id': request.website.id, } tz = self._get_visitor_timezone() if tz: vals['timezone'] = tz if not self.env.user._is_public(): vals['partner_id'] = self.env.user.partner_id.id vals['name'] = self.env.user.partner_id.name return self.sudo().create(vals) def _link_to_partner(self, partner, update_values=None): """ Link visitors to a partner. This method is meant to be overridden in order to propagate, if necessary, partner information to sub records. :param partner: partner used to link sub records; :param update_values: optional values to update visitors to link; """ vals = {'name': partner.name} if update_values: vals.update(update_values) self.write(vals) def _link_to_visitor(self, target, keep_unique=True): """ Link visitors to target visitors, because they are linked to the same identity. Purpose is mainly to propagate partner identity to sub records to ease database update and decide what to do with "duplicated". THis method is meant to be overridden in order to implement some specific behavior linked to sub records of duplicate management. :param target: main visitor, target of link process; :param keep_unique: if True, find a way to make target unique; """ # Link sub records of self to target partner if target.partner_id: self._link_to_partner(target.partner_id) # Link sub records of self to target visitor self.website_track_ids.write({'visitor_id': target.id}) if keep_unique: self.unlink() return target def _cron_archive_visitors(self): delay_days = int(self.env['ir.config_parameter'].sudo().get_param( 'website.visitor.live.days', 30)) deadline = datetime.now() - timedelta(days=delay_days) visitors_to_archive = self.env['website.visitor'].sudo().search([ ('last_connection_datetime', '<', deadline) ]) visitors_to_archive.write({'active': False}) def _update_visitor_last_visit(self): """ We need to do this part here to avoid concurrent updates error. """ try: with self.env.cr.savepoint(): query_lock = "SELECT * FROM website_visitor where id = %s FOR NO KEY UPDATE NOWAIT" self.env.cr.execute(query_lock, (self.id, ), log_exceptions=False) date_now = datetime.now() query = "UPDATE website_visitor SET " if self.last_connection_datetime < (date_now - timedelta(hours=8)): query += "visit_count = visit_count + 1," query += """ active = True, last_connection_datetime = %s WHERE id = %s """ self.env.cr.execute(query, (date_now, self.id), log_exceptions=False) except Exception: pass def _get_visitor_timezone(self): tz = request.httprequest.cookies.get('tz') if request else None if tz in pytz.all_timezones: return tz elif not self.env.user._is_public(): return self.env.user.tz else: return None
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 Flectra 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 Flectra 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: last_session = self.env['pos.session'].search( [('config_id', '=', config.id)], limit=1) if (not last_session) or (last_session.state == 'closed'): result.append( (config.id, config.name + ' (' + _('not used') + ')')) continue result.append( (config.id, config.name + ' (' + last_session.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): result = super(PosConfig, self).write(vals) config_display = self.filtered( lambda c: c.is_posbox and c.iface_customer_facing_display and not ( c.customer_facing_display_html or '').strip()) if config_display: super(PosConfig, config_display).write({ 'customer_facing_display_html': self._compute_default_customer_html() }) 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 PurchaseReport(models.Model): _name = "purchase.report" _description = "Purchase Report" _auto = False _order = 'date_order desc, price_total desc' date_order = fields.Datetime( 'Order Date', readonly=True, help= "Depicts the date when the Quotation should be validated and converted into a purchase order." ) state = fields.Selection([('draft', 'Draft RFQ'), ('sent', 'RFQ Sent'), ('to approve', 'To Approve'), ('purchase', 'Purchase Order'), ('done', 'Done'), ('cancel', 'Cancelled')], 'Status', readonly=True) product_id = fields.Many2one('product.product', 'Product', readonly=True) partner_id = fields.Many2one('res.partner', 'Vendor', readonly=True) date_approve = fields.Datetime('Confirmation Date', readonly=True) product_uom = fields.Many2one('uom.uom', 'Reference Unit of Measure', required=True) company_id = fields.Many2one('res.company', 'Company', readonly=True) currency_id = fields.Many2one('res.currency', 'Currency', readonly=True) user_id = fields.Many2one('res.users', 'Purchase Representative', readonly=True) delay = fields.Float( 'Days to Confirm', digits=(16, 2), readonly=True, group_operator='avg', help="Amount of time between purchase approval and order by date.") delay_pass = fields.Float( 'Days to Receive', digits=(16, 2), readonly=True, group_operator='avg', help= "Amount of time between date planned and order by date for each purchase order line." ) avg_days_to_purchase = fields.Float( 'Average Days to Purchase', digits=(16, 2), readonly=True, store= False, # needs store=False to prevent showing up as a 'measure' option help= "Amount of time between purchase approval and document creation date. Due to a hack needed to calculate this, \ every record will show the same average value, therefore only use this as an aggregated value with group_operator=avg" ) price_total = fields.Float('Total', readonly=True) price_average = fields.Float('Average Cost', readonly=True, group_operator="avg") nbr_lines = fields.Integer('# of Lines', readonly=True) category_id = fields.Many2one('product.category', 'Product Category', readonly=True) product_tmpl_id = fields.Many2one('product.template', 'Product Template', readonly=True) country_id = fields.Many2one('res.country', 'Partner Country', readonly=True) fiscal_position_id = fields.Many2one('account.fiscal.position', string='Fiscal Position', readonly=True) account_analytic_id = fields.Many2one('account.analytic.account', 'Analytic Account', readonly=True) commercial_partner_id = fields.Many2one('res.partner', 'Commercial Entity', readonly=True) weight = fields.Float('Gross Weight', readonly=True) volume = fields.Float('Volume', readonly=True) order_id = fields.Many2one('purchase.order', 'Order', readonly=True) untaxed_total = fields.Float('Untaxed Total', readonly=True) qty_ordered = fields.Float('Qty Ordered', readonly=True) qty_received = fields.Float('Qty Received', readonly=True) qty_billed = fields.Float('Qty Billed', readonly=True) qty_to_be_billed = fields.Float('Qty to be Billed', readonly=True) @property def _table_query(self): ''' Report needs to be dynamic to take into account multi-company selected + multi-currency rates ''' return '%s %s %s' % (self._select(), self._from(), self._group_by()) def _select(self): select_str = """ WITH currency_rate as (%s) SELECT po.id as order_id, min(l.id) as id, po.date_order as date_order, po.state, po.date_approve, po.dest_address_id, po.partner_id as partner_id, po.user_id as user_id, po.company_id as company_id, po.fiscal_position_id as fiscal_position_id, l.product_id, p.product_tmpl_id, t.categ_id as category_id, po.currency_id, t.uom_id as product_uom, extract(epoch from age(po.date_approve,po.date_order))/(24*60*60)::decimal(16,2) as delay, extract(epoch from age(l.date_planned,po.date_order))/(24*60*60)::decimal(16,2) as delay_pass, count(*) as nbr_lines, sum(l.price_total / COALESCE(po.currency_rate, 1.0))::decimal(16,2) * currency_table.rate as price_total, (sum(l.product_qty * l.price_unit / COALESCE(po.currency_rate, 1.0))/NULLIF(sum(l.product_qty/line_uom.factor*product_uom.factor),0.0))::decimal(16,2) * currency_table.rate as price_average, partner.country_id as country_id, partner.commercial_partner_id as commercial_partner_id, analytic_account.id as account_analytic_id, sum(p.weight * l.product_qty/line_uom.factor*product_uom.factor) as weight, sum(p.volume * l.product_qty/line_uom.factor*product_uom.factor) as volume, sum(l.price_subtotal / COALESCE(po.currency_rate, 1.0))::decimal(16,2) * currency_table.rate as untaxed_total, sum(l.product_qty / line_uom.factor * product_uom.factor) as qty_ordered, sum(l.qty_received / line_uom.factor * product_uom.factor) as qty_received, sum(l.qty_invoiced / line_uom.factor * product_uom.factor) as qty_billed, case when t.purchase_method = 'purchase' then sum(l.product_qty / line_uom.factor * product_uom.factor) - sum(l.qty_invoiced / line_uom.factor * product_uom.factor) else sum(l.qty_received / line_uom.factor * product_uom.factor) - sum(l.qty_invoiced / line_uom.factor * product_uom.factor) end as qty_to_be_billed """ % self.env['res.currency']._select_companies_rates() return select_str def _from(self): from_str = """ FROM purchase_order_line l join purchase_order po on (l.order_id=po.id) join res_partner partner on po.partner_id = partner.id left join product_product p on (l.product_id=p.id) left join product_template t on (p.product_tmpl_id=t.id) left join uom_uom line_uom on (line_uom.id=l.product_uom) left join uom_uom product_uom on (product_uom.id=t.uom_id) left join account_analytic_account analytic_account on (l.account_analytic_id = analytic_account.id) left join currency_rate cr on (cr.currency_id = po.currency_id and cr.company_id = po.company_id and cr.date_start <= coalesce(po.date_order, now()) and (cr.date_end is null or cr.date_end > coalesce(po.date_order, now()))) left join {currency_table} ON currency_table.company_id = po.company_id """.format( currency_table=self.env['res.currency']._get_query_currency_table({ 'multi_company': True, 'date': { 'date_to': fields.Date.today() } }), ) return from_str def _group_by(self): group_by_str = """ GROUP BY po.company_id, po.user_id, po.partner_id, line_uom.factor, po.currency_id, l.price_unit, po.date_approve, l.date_planned, l.product_uom, po.dest_address_id, po.fiscal_position_id, l.product_id, p.product_tmpl_id, t.categ_id, po.date_order, po.state, line_uom.uom_type, line_uom.category_id, t.uom_id, t.purchase_method, line_uom.id, product_uom.factor, partner.country_id, partner.commercial_partner_id, analytic_account.id, po.id, currency_table.rate """ return group_by_str @api.model def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True): """ This is a hack to allow us to correctly calculate the average of PO specific date values since the normal report query result will duplicate PO values across its PO lines during joins and lead to incorrect aggregation values. Only the AVG operator is supported for avg_days_to_purchase. """ avg_days_to_purchase = next( (field for field in fields if re.search(r'\bavg_days_to_purchase\b', field)), False) if avg_days_to_purchase: fields.remove(avg_days_to_purchase) if any( field.split(':')[1].split('(')[0] != 'avg' for field in [avg_days_to_purchase] if field): raise UserError( "Value: 'avg_days_to_purchase' should only be used to show an average. If you are seeing this message then it is being accessed incorrectly." ) res = [] if fields: res = super(PurchaseReport, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy) if not res and avg_days_to_purchase: res = [{}] if avg_days_to_purchase: self.check_access_rights('read') query = """ SELECT AVG(days_to_purchase.po_days_to_purchase)::decimal(16,2) AS avg_days_to_purchase FROM ( SELECT extract(epoch from age(po.date_approve,po.create_date))/(24*60*60) AS po_days_to_purchase FROM purchase_order po WHERE po.id IN ( SELECT "purchase_report"."order_id" FROM %s WHERE %s) ) AS days_to_purchase """ subdomain = AND([ domain, [('company_id', '=', self.env.company.id), ('date_approve', '!=', False)] ]) subtables, subwhere, subparams = expression(subdomain, self).query.get_sql() self.env.cr.execute(query % (subtables, subwhere), subparams) res[0].update({ '__count': 1, avg_days_to_purchase.split(':')[0]: self.env.cr.fetchall()[0][0], }) return res
class OpAssignmentSubLine(models.Model): _name = 'op.assignment.sub.line' _inherit = 'mail.thread' _rec_name = 'assignment_id' _description = 'Assignment Submission' assignment_id = fields.Many2one('op.assignment', 'Assignment', required=True) student_id = fields.Many2one('op.student', 'Student', default=lambda self: self.env['op.student']. search([('user_id', '=', self.env.uid)]), required=True) description = fields.Text('Description', track_visibility='onchange') state = fields.Selection([('draft', 'Draft'), ('submit', 'Submitted'), ('reject', 'Rejected'), ('change', 'Change Req.'), ('accept', 'Accepted')], 'State', default='draft', track_visibility='onchange') submission_date = fields.Datetime( 'Submission Date', readonly=True, default=lambda self: fields.Datetime.now(), required=True) marks = fields.Float('Marks', track_visibility='onchange') note = fields.Text('Note') user_id = fields.Many2one('res.users', related='student_id.user_id', string='User') faculty_user_id = fields.Many2one( 'res.users', related='assignment_id.faculty_id.user_id', string='Faculty User') @api.multi def act_draft(self): result = self.state = 'draft' return result and result or False @api.multi def act_submit(self): result = self.state = 'submit' return result and result or False @api.multi def act_accept(self): result = self.state = 'accept' return result and result or False @api.multi def act_change_req(self): result = self.state = 'change' return result and result or False @api.multi def act_reject(self): result = self.state = 'reject' return result and result or False @api.multi def unlink(self): for record in self: if not record.state == 'draft' and not self.env.user.has_group( 'openeducat_core.group_op_faculty'): raise ValidationError( _("You can't delete none draft submissions!")) res = super(OpAssignmentSubLine, self).unlink() return res @api.model def create(self, vals): if self.env.user.child_ids: raise Warning( _('Invalid Action!\n Parent can not \ create Assignment Submissions!')) return super(OpAssignmentSubLine, self).create(vals) @api.multi def write(self, vals): if self.env.user.child_ids: raise Warning( _('Invalid Action!\n Parent can not edit \ Assignment Submissions!')) return super(OpAssignmentSubLine, self).write(vals)
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() chosen_name = default.get('name') if default else '' default_name = chosen_name or _('%s (copy)') % self.name default = dict(default or {}, name=default_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 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.")) if 'usage' in values or 'scrap_location' in values: modified_locations = self.filtered( lambda l: any(l[f] != values[f] if f in values else False for f in {'usage', 'scrap_location'})) reserved_quantities = self.env['stock.move.line'].search_count([ ('location_id', 'in', modified_locations.ids), ('product_qty', '>', 0), ]) if reserved_quantities: raise UserError(_( "You cannot change the location type or its use as a scrap" " location as there are products reserved in this location." " Please unreserve the products first." )) 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 warehouse_id.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 AccountInvoiceRelate(models.Model): _inherit = 'account.invoice' insurance_id = fields.Many2one('insurance.details', string='Insurance') claim_id = fields.Many2one('claim.details', string='Insurance')
class PickingType(models.Model): _name = "stock.picking.type" _description = "The operation type determines the picking view" _order = 'sequence, id' name = fields.Char('Operation Types Name', required=True, translate=True) color = fields.Integer('Color') sequence = fields.Integer('Sequence', help="Used to order the 'All Operations' kanban view") sequence_id = fields.Many2one('ir.sequence', 'Reference Sequence', required=True) default_location_src_id = fields.Many2one( 'stock.location', 'Default Source Location', help="This is the default source location when you create a picking manually with this operation type. It is possible however to change it or that the routes put another location. If it is empty, it will check for the supplier location on the partner. ") default_location_dest_id = fields.Many2one( 'stock.location', 'Default Destination Location', help="This is the default destination location when you create a picking manually with this operation type. It is possible however to change it or that the routes put another location. If it is empty, it will check for the customer location on the partner. ") code = fields.Selection([('incoming', 'Vendors'), ('outgoing', 'Customers'), ('internal', 'Internal')], 'Type of Operation', required=True) return_picking_type_id = fields.Many2one('stock.picking.type', 'Operation Type for Returns') show_entire_packs = fields.Boolean('Allow moving packs', help="If checked, this shows the packs to be moved as a whole in the Operations tab all the time, even if there was no entire pack reserved.") warehouse_id = fields.Many2one( 'stock.warehouse', 'Warehouse', ondelete='cascade', default=lambda self: self.env['stock.warehouse'].search([('company_id', '=', self.env.user.company_id.id)], limit=1)) active = fields.Boolean('Active', default=True) use_create_lots = fields.Boolean( 'Create New Lots/Serial Numbers', default=True, help="If this is checked only, it will suppose you want to create new Lots/Serial Numbers, so you can provide them in a text field. ") use_existing_lots = fields.Boolean( 'Use Existing Lots/Serial Numbers', default=True, help="If this is checked, you will be able to choose the Lots/Serial Numbers. You can also decide to not put lots in this operation type. This means it will create stock with no lot or not put a restriction on the lot taken. ") show_operations = fields.Boolean( 'Show Detailed Operations', default=False, help="If this checkbox is ticked, the pickings lines will represent detailed stock operations. If not, the picking lines will represent an aggregate of detailed stock operations.") show_reserved = fields.Boolean( 'Show Reserved', default=True, help="If this checkbox is ticked, Flectra will show which products are reserved (lot/serial number, source location, source package).") # Statistics for the kanban view last_done_picking = fields.Char('Last 10 Done Pickings', compute='_compute_last_done_picking') count_picking_draft = fields.Integer(compute='_compute_picking_count') count_picking_ready = fields.Integer(compute='_compute_picking_count') count_picking = fields.Integer(compute='_compute_picking_count') count_picking_waiting = fields.Integer(compute='_compute_picking_count') count_picking_late = fields.Integer(compute='_compute_picking_count') count_picking_backorders = fields.Integer(compute='_compute_picking_count') rate_picking_late = fields.Integer(compute='_compute_picking_count') rate_picking_backorders = fields.Integer(compute='_compute_picking_count') barcode_nomenclature_id = fields.Many2one( 'barcode.nomenclature', 'Barcode Nomenclature') @api.one def _compute_last_done_picking(self): # TDE TODO: true multi tristates = [] for picking in self.env['stock.picking'].search([('picking_type_id', '=', self.id), ('state', '=', 'done')], order='date_done desc', limit=10): if picking.date_done > picking.date: tristates.insert(0, {'tooltip': picking.name or '' + ": " + _('Late'), 'value': -1}) elif picking.backorder_id: tristates.insert(0, {'tooltip': picking.name or '' + ": " + _('Backorder exists'), 'value': 0}) else: tristates.insert(0, {'tooltip': picking.name or '' + ": " + _('OK'), 'value': 1}) self.last_done_picking = json.dumps(tristates) def _compute_picking_count(self): # TDE TODO count picking can be done using previous two domains = { 'count_picking_draft': [('state', '=', 'draft')], 'count_picking_waiting': [('state', 'in', ('confirmed', 'waiting'))], 'count_picking_ready': [('state', '=', 'assigned')], 'count_picking': [('state', 'in', ('assigned', 'waiting', 'confirmed'))], 'count_picking_late': [('scheduled_date', '<', time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)), ('state', 'in', ('assigned', 'waiting', 'confirmed'))], 'count_picking_backorders': [('backorder_id', '!=', False), ('state', 'in', ('confirmed', 'assigned', 'waiting'))], } for field in domains: data = self.env['stock.picking'].read_group(domains[field] + [('state', 'not in', ('done', 'cancel')), ('picking_type_id', 'in', self.ids)], ['picking_type_id'], ['picking_type_id']) count = { x['picking_type_id'][0]: x['picking_type_id_count'] for x in data if x['picking_type_id'] } for record in self: record[field] = count.get(record.id, 0) for record in self: record.rate_picking_late = record.count_picking and record.count_picking_late * 100 / record.count_picking or 0 record.rate_picking_backorders = record.count_picking and record.count_picking_backorders * 100 / record.count_picking or 0 def name_get(self): """ Display 'Warehouse_name: PickingType_name' """ # TDE TODO remove context key support + update purchase res = [] for picking_type in self: if self.env.context.get('special_shortened_wh_name'): if picking_type.warehouse_id: name = picking_type.warehouse_id.name else: name = _('Customer') + ' (' + picking_type.name + ')' elif picking_type.warehouse_id: name = picking_type.warehouse_id.name + ': ' + picking_type.name else: name = picking_type.name res.append((picking_type.id, name)) return res @api.model def name_search(self, name, args=None, operator='ilike', limit=100): args = args or [] domain = [] if name: domain = ['|', ('name', operator, name), ('warehouse_id.name', operator, name)] picks = self.search(domain + args, limit=limit) return picks.name_get() @api.onchange('code') def onchange_picking_code(self): if self.code == 'incoming': self.default_location_src_id = self.env.ref('stock.stock_location_suppliers').id self.default_location_dest_id = self.env.ref('stock.stock_location_stock').id elif self.code == 'outgoing': self.default_location_src_id = self.env.ref('stock.stock_location_stock').id self.default_location_dest_id = self.env.ref('stock.stock_location_customers').id @api.onchange('show_operations') def onchange_show_operations(self): if self.show_operations is True: self.show_reserved = True def _get_action(self, action_xmlid): # TDE TODO check to have one view + custo in methods action = self.env.ref(action_xmlid).read()[0] if self: action['display_name'] = self.display_name return action def get_action_picking_tree_late(self): return self._get_action('stock.action_picking_tree_late') def get_action_picking_tree_backorder(self): return self._get_action('stock.action_picking_tree_backorder') def get_action_picking_tree_waiting(self): return self._get_action('stock.action_picking_tree_waiting') def get_action_picking_tree_ready(self): return self._get_action('stock.action_picking_tree_ready') def get_stock_picking_action_picking_type(self): return self._get_action('stock.stock_picking_action_picking_type')