class ir_cron(models.Model): """ Model describing cron jobs (also called actions or tasks). """ # TODO: perhaps in the future we could consider a flag on ir.cron jobs # that would cause database wake-up even if the database has not been # loaded yet or was already unloaded (e.g. 'force_db_wakeup' or something) # See also eagle.cron _name = "ir.cron" _order = 'cron_name' _description = 'Scheduled Actions' ir_actions_server_id = fields.Many2one('ir.actions.server', 'Server action', delegate=True, ondelete='restrict', required=True) cron_name = fields.Char('Name', related='ir_actions_server_id.name', store=True, readonly=False) user_id = fields.Many2one('res.users', string='Scheduler User', default=lambda self: self.env.user, required=True) active = fields.Boolean(default=True) interval_number = fields.Integer(default=1, help="Repeat every x.") interval_type = fields.Selection([('minutes', 'Minutes'), ('hours', 'Hours'), ('days', 'Days'), ('weeks', 'Weeks'), ('months', 'Months')], string='Interval Unit', default='months') numbercall = fields.Integer( string='Number of Calls', default=1, help= 'How many times the method is called,\na negative number indicates no limit.' ) doall = fields.Boolean( string='Repeat Missed', help= "Specify if missed occurrences should be executed when the server restarts." ) nextcall = fields.Datetime( string='Next Execution Date', required=True, default=fields.Datetime.now, help="Next planned execution date for this job.") lastcall = fields.Datetime( string='Last Execution Date', help= "Previous time the cron ran successfully, provided to the job through the context on the `lastcall` key" ) priority = fields.Integer( default=5, help= 'The priority of the job, as an integer: 0 means higher priority, 10 means lower priority.' ) @api.model def create(self, values): values['usage'] = 'ir_cron' return super(ir_cron, self).create(values) def method_direct_trigger(self): self.check_access_rights('write') for cron in self: self.with_user(cron.user_id).ir_actions_server_id.run() return True @api.model def _handle_callback_exception(self, cron_name, server_action_id, job_id, job_exception): """ Method called when an exception is raised by a job. Simply logs the exception and rollback the transaction. """ self._cr.rollback() @api.model def _callback(self, cron_name, server_action_id, job_id): """ Run the method associated to a given job. It takes care of logging and exception handling. Note that the user running the server action is the user calling this method. """ try: if self.pool != self.pool.check_signaling(): # the registry has changed, reload self in the new registry self.env.reset() self = self.env()[self._name] log_depth = (None if _logger.isEnabledFor(logging.DEBUG) else 1) eagle.netsvc.log( _logger, logging.DEBUG, 'cron.object.execute', (self._cr.dbname, self._uid, '*', cron_name, server_action_id), depth=log_depth) start_time = False if _logger.isEnabledFor(logging.DEBUG): start_time = time.time() self.env['ir.actions.server'].browse(server_action_id).run() if start_time and _logger.isEnabledFor(logging.DEBUG): end_time = time.time() _logger.debug('%.3fs (cron %s, server action %d with uid %d)', end_time - start_time, cron_name, server_action_id, self.env.uid) self.pool.signal_changes() except Exception as e: self.pool.reset_changes() _logger.exception( "Call from cron %s for server action #%s failed in Job #%s", cron_name, server_action_id, job_id) self._handle_callback_exception(cron_name, server_action_id, job_id, e) @classmethod def _process_job(cls, job_cr, job, cron_cr): """ Run a given job taking care of the repetition. :param job_cr: cursor to use to execute the job, safe to commit/rollback :param job: job to be run (as a dictionary). :param cron_cr: cursor holding lock on the cron job row, to use to update the next exec date, must not be committed/rolled back! """ try: with api.Environment.manage(): cron = api.Environment( job_cr, job['user_id'], {'lastcall': fields.Datetime.from_string(job['lastcall']) })[cls._name] # Use the user's timezone to compare and compute datetimes, # otherwise unexpected results may appear. For instance, adding # 1 month in UTC to July 1st at midnight in GMT+2 gives July 30 # instead of August 1st! now = fields.Datetime.context_timestamp(cron, datetime.now()) nextcall = fields.Datetime.context_timestamp( cron, fields.Datetime.from_string(job['nextcall'])) numbercall = job['numbercall'] ok = False while nextcall < now and numbercall: if numbercall > 0: numbercall -= 1 if not ok or job['doall']: cron._callback(job['cron_name'], job['ir_actions_server_id'], job['id']) if numbercall: nextcall += _intervalTypes[job['interval_type']]( job['interval_number']) ok = True addsql = '' if not numbercall: addsql = ', active=False' cron_cr.execute( "UPDATE ir_cron SET nextcall=%s, numbercall=%s, lastcall=%s" + addsql + " WHERE id=%s", (fields.Datetime.to_string(nextcall.astimezone( pytz.UTC)), numbercall, fields.Datetime.to_string(now.astimezone( pytz.UTC)), job['id'])) cron.flush() cron.invalidate_cache() finally: job_cr.commit() cron_cr.commit() @classmethod def _process_jobs(cls, db_name): """ Try to process all cron jobs. This selects in database all the jobs that should be processed. It then tries to lock each of them and, if it succeeds, run the cron job (if it doesn't succeed, it means the job was already locked to be taken care of by another thread) and return. :raise BadVersion: if the version is different from the worker's :raise BadModuleState: if modules are to install/upgrade/remove """ db = eagle.sql_db.db_connect(db_name) threading.current_thread().dbname = db_name try: with db.cursor() as cr: # Make sure the database has the same version as the code of # base and that no module must be installed/upgraded/removed cr.execute( "SELECT latest_version FROM ir_module_module WHERE name=%s", ['base']) (version, ) = cr.fetchone() cr.execute( "SELECT COUNT(*) FROM ir_module_module WHERE state LIKE %s", ['to %']) (changes, ) = cr.fetchone() if version is None: raise BadModuleState() elif version != BASE_VERSION: raise BadVersion() # Careful to compare timestamps with 'UTC' - everything is UTC as of v6.1. cr.execute("""SELECT * FROM ir_cron WHERE numbercall != 0 AND active AND nextcall <= (now() at time zone 'UTC') ORDER BY priority""") jobs = cr.dictfetchall() if changes: if not jobs: raise BadModuleState() # nextcall is never updated if the cron is not executed, # it is used as a sentinel value to check whether cron jobs # have been locked for a long time (stuck) parse = fields.Datetime.from_string oldest = min([parse(job['nextcall']) for job in jobs]) if datetime.now() - oldest > MAX_FAIL_TIME: eagle.modules.reset_modules_state(db_name) else: raise BadModuleState() for job in jobs: lock_cr = db.cursor() try: # Try to grab an exclusive lock on the job row from within the task transaction # Restrict to the same conditions as for the search since the job may have already # been run by an other thread when cron is running in multi thread lock_cr.execute("""SELECT * FROM ir_cron WHERE numbercall != 0 AND active AND nextcall <= (now() at time zone 'UTC') AND id=%s FOR UPDATE NOWAIT""", (job['id'], ), log_exceptions=False) locked_job = lock_cr.fetchone() if not locked_job: _logger.debug( "Job `%s` already executed by another process/thread. skipping it", job['cron_name']) continue # Got the lock on the job row, run its code _logger.info('Starting job `%s`.', job['cron_name']) job_cr = db.cursor() try: registry = eagle.registry(db_name) registry[cls._name]._process_job(job_cr, job, lock_cr) _logger.info('Job `%s` done.', job['cron_name']) except Exception: _logger.exception( 'Unexpected exception while processing cron job %r', job) finally: job_cr.close() except psycopg2.OperationalError as e: if e.pgcode == '55P03': # Class 55: Object not in prerequisite state; 55P03: lock_not_available _logger.debug( 'Another process/thread is already busy executing job `%s`, skipping it.', job['cron_name']) continue else: # Unexpected OperationalError raise finally: # we're exiting due to an exception while acquiring the lock lock_cr.close() finally: if hasattr(threading.current_thread(), 'dbname'): del threading.current_thread().dbname @classmethod def _acquire_job(cls, db_name): """ Try to process all cron jobs. This selects in database all the jobs that should be processed. It then tries to lock each of them and, if it succeeds, run the cron job (if it doesn't succeed, it means the job was already locked to be taken care of by another thread) and return. This method hides most exceptions related to the database's version, the modules' state, and such. """ try: cls._process_jobs(db_name) except BadVersion: _logger.warning( 'Skipping database %s as its base version is not %s.', db_name, BASE_VERSION) except BadModuleState: _logger.warning( 'Skipping database %s because of modules to install/upgrade/remove.', db_name) except psycopg2.ProgrammingError as e: if e.pgcode == '42P01': # Class 42 — Syntax Error or Access Rule Violation; 42P01: undefined_table # The table ir_cron does not exist; this is probably not an OpenERP database. _logger.warning( 'Tried to poll an undefined table on database %s.', db_name) else: raise except Exception: _logger.warning('Exception in cron:', exc_info=True) def _try_lock(self): """Try to grab a dummy exclusive write-lock to the rows with the given ids, to make sure a following write() or unlink() will not block due to a process currently executing those cron tasks""" try: self._cr.execute( """SELECT id FROM "%s" WHERE id IN %%s FOR UPDATE NOWAIT""" % self._table, [tuple(self.ids)], log_exceptions=False) except psycopg2.OperationalError: self._cr.rollback( ) # early rollback to allow translations to work for the user feedback raise UserError( _("Record cannot be modified right now: " "This cron task is currently being executed and may not be modified " "Please try again in a few minutes")) def write(self, vals): self._try_lock() return super(ir_cron, self).write(vals) def unlink(self): self._try_lock() return super(ir_cron, self).unlink() def try_write(self, values): try: with self._cr.savepoint(): self._cr.execute( """SELECT id FROM "%s" WHERE id IN %%s FOR UPDATE NOWAIT""" % self._table, [tuple(self.ids)], log_exceptions=False) except psycopg2.OperationalError: pass else: return super(ir_cron, self).write(values) return False @api.model def toggle(self, model, domain): active = bool(self.env[model].search_count(domain)) return self.try_write({'active': active})
class ProductImage(models.Model): _name = 'product.image' _description = "Product Image" _inherit = ['image.mixin'] _order = 'sequence, id' name = fields.Char("Name", required=True) sequence = fields.Integer(default=10, index=True) image_1920 = fields.Image(required=True) product_tmpl_id = fields.Many2one('product.template', "Product Template", index=True, ondelete='cascade') product_variant_id = fields.Many2one('product.product', "Product Variant", index=True, ondelete='cascade') image_1920_id = fields.Many2one('res.partner', "Partner Image", index=True, ondelete='cascade') video_url = fields.Char('Video URL', help='URL of a video for showcasing your product.') embed_code = fields.Char(compute="_compute_embed_code") can_image_1024_be_zoomed = fields.Boolean( "Can Image 1024 be zoomed", compute='_compute_can_image_1024_be_zoomed', store=True) @api.depends('image_1920', 'image_1024') def _compute_can_image_1024_be_zoomed(self): for image in self: image.can_image_1024_be_zoomed = image.image_1920 and tools.is_image_size_above( image.image_1920, image.image_1024) @api.depends('video_url') def _compute_embed_code(self): for image in self: image.embed_code = get_video_embed_code(image.video_url) @api.constrains('video_url') def _check_valid_video_url(self): for image in self: if image.video_url and not image.embed_code: raise ValidationError( _("Provided video URL for '%s' is not valid. Please enter a valid video URL." ) % image.name) @api.model_create_multi def create(self, vals_list): """ We don't want the default_product_tmpl_id from the context to be applied if we have a product_variant_id set to avoid having the variant images to show also as template images. But we want it if we don't have a product_variant_id set. """ context_without_template = self.with_context({ k: v for k, v in self.env.context.items() if k != 'default_product_tmpl_id' }) normal_vals = [] variant_vals_list = [] for vals in vals_list: if vals.get('product_variant_id' ) and 'default_product_tmpl_id' in self.env.context: variant_vals_list.append(vals) else: normal_vals.append(vals) return super().create(normal_vals) + super( ProductImage, context_without_template).create(variant_vals_list)
class UoM(models.Model): _name = 'uom.uom' _description = 'Product Unit of Measure' _order = "name" name = fields.Char('Unit of Measure', required=True, translate=True) category_id = fields.Many2one( 'uom.category', 'Category', required=True, ondelete='cascade', help= "Conversion between Units of Measure can only occur if they belong to the same category. The conversion will be made based on the ratios." ) factor = fields.Float( 'Ratio', default=1.0, digits=0, required=True, # force NUMERIC with unlimited precision help= 'How much bigger or smaller this unit is compared to the reference Unit of Measure for this category: 1 * (reference unit) = ratio * (this unit)' ) factor_inv = fields.Float( 'Bigger Ratio', compute='_compute_factor_inv', digits=0, # force NUMERIC with unlimited precision readonly=True, required=True, help= 'How many times this Unit of Measure is bigger than the reference Unit of Measure in this category: 1 * (this unit) = ratio * (reference unit)' ) rounding = fields.Float( 'Rounding Precision', default=0.01, digits=0, required=True, help="The computed quantity will be a multiple of this value. " "Use 1.0 for a Unit of Measure that cannot be further split, such as a piece." ) active = fields.Boolean( 'Active', default=True, help= "Uncheck the active field to disable a unit of measure without deleting it." ) uom_type = fields.Selection( [('bigger', 'Bigger than the reference Unit of Measure'), ('reference', 'Reference Unit of Measure for this category'), ('smaller', 'Smaller than the reference Unit of Measure')], 'Type', default='reference', required=1) measure_type = fields.Selection(string="Type of measurement category", related='category_id.measure_type', store=True, readonly=True) _sql_constraints = [ ('factor_gt_zero', 'CHECK (factor!=0)', 'The conversion ratio for a unit of measure cannot be 0!'), ('rounding_gt_zero', 'CHECK (rounding>0)', 'The rounding precision must be strictly positive.'), ('factor_reference_is_one', "CHECK((uom_type = 'reference' AND factor = 1.0) OR (uom_type != 'reference'))", "The reference unit must have a conversion factor equal to 1.") ] @api.one @api.depends('factor') def _compute_factor_inv(self): self.factor_inv = self.factor and (1.0 / self.factor) or 0.0 @api.onchange('uom_type') def _onchange_uom_type(self): if self.uom_type == 'reference': self.factor = 1 @api.constrains('category_id', 'uom_type', 'active') def _check_category_reference_uniqueness(self): """ Force the existence of only one UoM reference per category NOTE: this is a constraint on the all table. This might not be a good practice, but this is not possible to do it in SQL directly. """ category_ids = self.mapped('category_id').ids self._cr.execute( """ SELECT C.id AS category_id, count(U.id) AS uom_count FROM uom_category C LEFT JOIN uom_uom U ON C.id = U.category_id AND uom_type = 'reference' AND U.active = 't' WHERE C.id IN %s GROUP BY C.id """, (tuple(category_ids), )) for uom_data in self._cr.dictfetchall(): if uom_data['uom_count'] == 0: raise ValidationError( _("UoM category %s should have a reference unit of measure. If you just created a new category, please record the 'reference' unit first." ) % (self.env['uom.category'].browse( uom_data['category_id']).name, )) if uom_data['uom_count'] > 1: raise ValidationError( _("UoM category %s should only have one reference unit of measure." ) % (self.env['uom.category'].browse( uom_data['category_id']).name, )) @api.model_create_multi def create(self, vals_list): for values in vals_list: if 'factor_inv' in values: factor_inv = values.pop('factor_inv') values['factor'] = factor_inv and (1.0 / factor_inv) or 0.0 return super(UoM, self).create(vals_list) @api.multi def write(self, values): if 'factor_inv' in values: factor_inv = values.pop('factor_inv') values['factor'] = factor_inv and (1.0 / factor_inv) or 0.0 return super(UoM, self).write(values) @api.multi def unlink(self): if self.filtered(lambda uom: uom.measure_type == 'time'): raise UserError( _("You cannot delete this UoM as it is used by the system. You should rather archive it." )) return super(UoM, self).unlink() @api.model def name_create(self, name): """ The UoM category and factor are required, so we'll have to add temporary values for imported UoMs """ values = {self._rec_name: name, 'factor': 1} # look for the category based on the english name, i.e. no context on purpose! # TODO: should find a way to have it translated but not created until actually used if not self._context.get('default_category_id'): EnglishUoMCateg = self.env['uom.category'].with_context({}) misc_category = EnglishUoMCateg.search([ ('name', '=', 'Unsorted/Imported Units') ]) if misc_category: values['category_id'] = misc_category.id else: values['category_id'] = EnglishUoMCateg.name_create( 'Unsorted/Imported Units')[0] new_uom = self.create(values) return new_uom.name_get()[0] @api.multi def _compute_quantity(self, qty, to_unit, round=True, rounding_method='UP', raise_if_failure=True): """ Convert the given quantity from the current UoM `self` into a given one :param qty: the quantity to convert :param to_unit: the destination UoM record (uom.uom) :param raise_if_failure: only if the conversion is not possible - if true, raise an exception if the conversion is not possible (different UoM category), - otherwise, return the initial quantity """ if not self: return qty self.ensure_one() if self.category_id.id != to_unit.category_id.id: if raise_if_failure: raise UserError( _('The unit of measure %s defined on the order line doesn\'t belong to the same category than the unit of measure %s defined on the product. Please correct the unit of measure defined on the order line or on the product, they should belong to the same category.' ) % (self.name, to_unit.name)) else: return qty amount = qty / self.factor if to_unit: amount = amount * to_unit.factor if round: amount = tools.float_round(amount, precision_rounding=to_unit.rounding, rounding_method=rounding_method) return amount @api.multi def _compute_price(self, price, to_unit): self.ensure_one() if not self or not price or not to_unit or self == to_unit: return price if self.category_id.id != to_unit.category_id.id: return price amount = price * self.factor if to_unit: amount = amount / to_unit.factor return amount
class IrActionsActWindow(models.Model): _name = 'ir.actions.act_window' _description = 'Action Window' _table = 'ir_act_window' _inherit = 'ir.actions.actions' _sequence = 'ir_actions_id_seq' _order = 'name' @api.constrains('res_model', 'src_model') def _check_model(self): for action in self: if action.res_model not in self.env: raise ValidationError( _('Invalid model name %r in action definition.') % action.res_model) if action.src_model and action.src_model not in self.env: raise ValidationError( _('Invalid model name %r in action definition.') % action.src_model) @api.depends('view_ids.view_mode', 'view_mode', 'view_id.type') def _compute_views(self): """ Compute an ordered list of the specific view modes that should be enabled when displaying the result of this action, along with the ID of the specific view to use for each mode, if any were required. This function hides the logic of determining the precedence between the view_modes string, the view_ids o2m, and the view_id m2o that can be set on the action. """ for act in self: act.views = [(view.view_id.id, view.view_mode) for view in act.view_ids] got_modes = [view.view_mode for view in act.view_ids] all_modes = act.view_mode.split(',') missing_modes = [ mode for mode in all_modes if mode not in got_modes ] if missing_modes: if act.view_id.type in missing_modes: # reorder missing modes to put view_id first if present missing_modes.remove(act.view_id.type) act.views.append((act.view_id.id, act.view_id.type)) act.views.extend([(False, mode) for mode in missing_modes]) @api.depends('res_model', 'search_view_id') def _compute_search_view(self): for act in self: fvg = self.env[act.res_model].fields_view_get( act.search_view_id.id, 'search') act.search_view = str(fvg) name = fields.Char(string='Action Name', translate=True) type = fields.Char(default="ir.actions.act_window") view_id = fields.Many2one('ir.ui.view', string='View Ref.', ondelete='set null') domain = fields.Char( string='Domain Value', help= "Optional domain filtering of the destination data, as a Python expression" ) context = fields.Char( string='Context Value', default={}, required=True, help= "Context dictionary as Python expression, empty by default (Default: {})" ) res_id = fields.Integer( string='Record ID', help= "Database ID of record to open in form view, when ``view_mode`` is set to 'form' only" ) res_model = fields.Char( string='Destination Model', required=True, help="Model name of the object to open in the view window") src_model = fields.Char( string='Source Model', help= "Optional model name of the objects on which this action should be visible" ) target = fields.Selection([('current', 'Current Window'), ('new', 'New Window'), ('inline', 'Inline Edit'), ('fullscreen', 'Full Screen'), ('main', 'Main action of Current Window')], default="current", string='Target Window') view_mode = fields.Char( required=True, default='tree,form', help= "Comma-separated list of allowed view modes, such as 'form', 'tree', 'calendar', etc. (Default: tree,form)" ) view_type = fields.Selection( [('tree', 'Tree'), ('form', 'Form')], default="form", string='View Type', required=True, help= "View type: Tree type to use for the tree view, set to 'tree' for a hierarchical tree view, or 'form' for a regular list view" ) usage = fields.Char( string='Action Usage', help="Used to filter menu and home actions from the user form.") view_ids = fields.One2many('ir.actions.act_window.view', 'act_window_id', string='No of Views') views = fields.Binary(compute='_compute_views', help="This function field computes the ordered list of views that should be enabled " \ "when displaying the result of an action, federating view mode, views and " \ "reference view. The result is returned as an ordered list of pairs (view_id,view_mode).") limit = fields.Integer(default=80, help='Default limit for the list view') groups_id = fields.Many2many('res.groups', 'ir_act_window_group_rel', 'act_id', 'gid', string='Groups') search_view_id = fields.Many2one('ir.ui.view', string='Search View Ref.') filter = fields.Boolean() auto_search = fields.Boolean(default=True) search_view = fields.Text(compute='_compute_search_view') multi = fields.Boolean( string='Restrict to lists', help= "If checked and the action is bound to a model, it will only appear in the More menu on list views" ) @api.multi def read(self, fields=None, load='_classic_read'): """ call the method get_empty_list_help of the model and set the window action help message """ result = super(IrActionsActWindow, self).read(fields, load=load) if not fields or 'help' in fields: for values in result: model = values.get('res_model') if model in self.env: eval_ctx = dict(self.env.context) try: ctx = safe_eval(values.get('context', '{}'), eval_ctx) except: ctx = {} values['help'] = self.with_context( **ctx).env[model].get_empty_list_help( values.get('help', '')) return result @api.model def for_xml_id(self, module, xml_id): """ Returns the act_window object created for the provided xml_id :param module: the module the act_window originates in :param xml_id: the namespace-less id of the action (the @id attribute from the XML file) :return: A read() view of the ir.actions.act_window """ record = self.env.ref("%s.%s" % (module, xml_id)) return record.read()[0] @api.model_create_multi def create(self, vals_list): self.clear_caches() return super(IrActionsActWindow, self).create(vals_list) @api.multi def unlink(self): self.clear_caches() return super(IrActionsActWindow, self).unlink() @api.multi def exists(self): ids = self._existing() existing = self.filtered(lambda rec: rec.id in ids) if len(existing) < len(self): # mark missing records in cache with a failed value exc = MissingError( _("Record does not exist or has been deleted.") + '\n\n({} {}, {} {})'.format(_('Records:'), ( self - existing).ids[:6], _('User:'), self._uid)) for record in (self - existing): record._cache.set_failed(self._fields, exc) return existing @api.model @tools.ormcache() def _existing(self): self._cr.execute("SELECT id FROM %s" % self._table) return set(row[0] for row in self._cr.fetchall())
class HolidaysType(models.Model): _inherit = "hr.leave.type" def _default_project_id(self): company = self.company_id if self.company_id else self.env.company return company.leave_timesheet_project_id.id def _default_task_id(self): company = self.company_id if self.company_id else self.env.company return company.leave_timesheet_task_id.id timesheet_generate = fields.Boolean( 'Generate Timesheet', default=True, help= "If checked, when validating a time off, timesheet will be generated in the Vacation Project of the company." ) timesheet_project_id = fields.Many2one( 'project.project', string="Project", default=_default_project_id, domain="[('company_id', '=', company_id)]", help= "The project will contain the timesheet generated when a time off is validated." ) timesheet_task_id = fields.Many2one( 'project.task', string="Task for timesheet", default=_default_task_id, domain= "[('project_id', '=', timesheet_project_id), ('company_id', '=', company_id)]" ) @api.onchange('timesheet_task_id') def _onchange_timesheet_generate(self): if self.timesheet_task_id or self.timesheet_project_id: self.timesheet_generate = True else: self.timesheet_generate = False @api.onchange('timesheet_project_id') def _onchange_timesheet_project(self): company = self.company_id if self.company_id else self.env.company default_task_id = company.leave_timesheet_task_id if default_task_id and default_task_id.project_id == self.timesheet_project_id: self.timesheet_task_id = default_task_id else: self.timesheet_task_id = False if self.timesheet_project_id: self.timesheet_generate = True else: self.timesheet_generate = False @api.constrains('timesheet_generate', 'timesheet_project_id', 'timesheet_task_id') def _check_timesheet_generate(self): for holiday_status in self: if holiday_status.timesheet_generate: if not holiday_status.timesheet_project_id or not holiday_status.timesheet_task_id: raise ValidationError( _("Both the internal project and task are required to " "generate a timesheet for the time off. If you don't want a timesheet, you should " "leave the internal project and task empty."))
class PhoneMixin(models.AbstractModel): """ Purpose of this mixin is to offer two services * compute a sanitized phone number based on ´´_sms_get_number_fields´´. It takes first sanitized value, trying each field returned by the method (see ``MailThread._sms_get_number_fields()´´ for more details about the usage of this method); * compute blacklist state of records. It is based on phone.blacklist model and give an easy-to-use field and API to manipulate blacklisted records; Main API methods * ``_phone_set_blacklisted``: set recordset as blacklisted; * ``_phone_reset_blacklisted``: reactivate recordset (even if not blacklisted this method can be called safely); """ _name = 'mail.thread.phone' _description = 'Phone Blacklist Mixin' _inherit = ['mail.thread'] phone_sanitized = fields.Char( string='Sanitized Number', compute="_compute_phone_sanitized", compute_sudo=True, store=True, help="Field used to store sanitized phone number. Helps speeding up searches and comparisons.") phone_blacklisted = fields.Boolean( string='Phone Blacklisted', compute="_compute_phone_blacklisted", compute_sudo=True, store=False, search="_search_phone_blacklisted", groups="base.group_user", help="If the email address is on the blacklist, the contact won't receive mass mailing anymore, from any list") @api.depends(lambda self: self._phone_get_number_fields()) def _compute_phone_sanitized(self): self._assert_phone_field() number_fields = self._phone_get_number_fields() for record in self: for fname in number_fields: sanitized = record.phone_get_sanitized_number(number_fname=fname) if sanitized: break record.phone_sanitized = sanitized @api.depends('phone_sanitized') def _compute_phone_blacklisted(self): # TODO : Should remove the sudo as compute_sudo defined on methods. # But if user doesn't have access to mail.blacklist, doen't work without sudo(). blacklist = set(self.env['phone.blacklist'].sudo().search([ ('number', 'in', self.mapped('phone_sanitized'))]).mapped('number')) for record in self: record.phone_blacklisted = record.phone_sanitized in blacklist @api.model def _search_phone_blacklisted(self, operator, value): # Assumes operator is '=' or '!=' and value is True or False self._assert_phone_field() if operator != '=': if operator == '!=' and isinstance(value, bool): value = not value else: raise NotImplementedError() if value: query = """ SELECT m.id FROM phone_blacklist bl JOIN %s m ON m.phone_sanitized = bl.number AND bl.active """ else: query = """ SELECT m.id FROM %s m LEFT JOIN phone_blacklist bl ON m.phone_sanitized = bl.number AND bl.active WHERE bl.id IS NULL """ self._cr.execute(query % self._table) res = self._cr.fetchall() if not res: return [(0, '=', 1)] return [('id', 'in', [r[0] for r in res])] def _assert_phone_field(self): if not hasattr(self, "_phone_get_number_fields"): raise UserError(_('Invalid primary phone field on model %s') % self._name) if not any(fname in self and self._fields[fname].type == 'char' for fname in self._phone_get_number_fields()): raise UserError(_('Invalid primary phone field on model %s') % self._name) def _phone_get_number_fields(self): """ This method returns the fields to use to find the number to use to send an SMS on a record. """ return [] def _phone_get_country_field(self): if 'country_id' in self: return 'country_id' return False def phone_get_sanitized_numbers(self, number_fname='mobile', force_format='E164'): res = dict.fromkeys(self.ids, False) country_fname = self._phone_get_country_field() for record in self: number = record[number_fname] res[record.id] = phone_validation.phone_sanitize_numbers_w_record([number], record, record_country_fname=country_fname, force_format=force_format)[number]['sanitized'] return res def phone_get_sanitized_number(self, number_fname='mobile', force_format='E164'): self.ensure_one() country_fname = self._phone_get_country_field() number = self[number_fname] return phone_validation.phone_sanitize_numbers_w_record([number], self, record_country_fname=country_fname, force_format=force_format)[number]['sanitized'] def _phone_set_blacklisted(self): return self.env['phone.blacklist'].sudo()._add([r.phone_sanitized for r in self]) def _phone_reset_blacklisted(self): return self.env['phone.blacklist'].sudo()._remove([r.phone_sanitized for r in self])
class PosOrderReport(models.Model): _name = "report.pos.order" _description = "Point of Sale Orders Report" _auto = False _order = 'date desc' date = fields.Datetime(string='Order Date', readonly=True) order_id = fields.Many2one('pos.order', string='Order', readonly=True) partner_id = fields.Many2one('res.partner', string='Customer', readonly=True) product_id = fields.Many2one('product.product', string='Product', readonly=True) product_tmpl_id = fields.Many2one('product.template', string='Product Template', readonly=True) state = fields.Selection([('draft', 'New'), ('paid', 'Paid'), ('done', 'Posted'), ('invoiced', 'Invoiced'), ('cancel', 'Cancelled')], string='Status') user_id = fields.Many2one('res.users', string='Salesperson', readonly=True) price_total = fields.Float(string='Total Price', readonly=True) price_sub_total = fields.Float(string='Subtotal w/o discount', readonly=True) total_discount = fields.Float(string='Total Discount', readonly=True) average_price = fields.Float(string='Average Price', readonly=True, group_operator="avg") location_id = fields.Many2one('stock.location', string='Location', readonly=True) company_id = fields.Many2one('res.company', string='Company', readonly=True) nbr_lines = fields.Integer(string='Sale Line Count', readonly=True, oldname='nbr') product_qty = fields.Integer(string='Product Quantity', readonly=True) journal_id = fields.Many2one('account.journal', string='Journal') delay_validation = fields.Integer(string='Delay Validation') product_categ_id = fields.Many2one('product.category', string='Product Category', readonly=True) invoiced = fields.Boolean(readonly=True) config_id = fields.Many2one('pos.config', string='Point of Sale', readonly=True) pos_categ_id = fields.Many2one('pos.category', string='PoS Category', readonly=True) pricelist_id = fields.Many2one('product.pricelist', string='Pricelist', readonly=True) session_id = fields.Many2one('pos.session', string='Session', readonly=True) def _select(self): return """ SELECT MIN(l.id) AS id, COUNT(*) AS nbr_lines, s.date_order AS date, SUM(l.qty) AS product_qty, SUM(l.qty * l.price_unit) AS price_sub_total, SUM((l.qty * l.price_unit) * (100 - l.discount) / 100) AS price_total, SUM((l.qty * l.price_unit) * (l.discount / 100)) AS total_discount, (SUM(l.qty*l.price_unit)/SUM(l.qty * u.factor))::decimal AS average_price, SUM(cast(to_char(date_trunc('day',s.date_order) - date_trunc('day',s.create_date),'DD') AS INT)) AS delay_validation, s.id as order_id, s.partner_id AS partner_id, s.state AS state, s.user_id AS user_id, s.location_id AS location_id, s.company_id AS company_id, s.sale_journal AS journal_id, l.product_id AS product_id, pt.categ_id AS product_categ_id, p.product_tmpl_id, ps.config_id, pt.pos_categ_id, s.pricelist_id, s.session_id, s.invoice_id IS NOT NULL AS invoiced """ def _from(self): return """ FROM pos_order_line AS l LEFT JOIN pos_order s ON (s.id=l.order_id) LEFT JOIN product_product p ON (l.product_id=p.id) LEFT JOIN product_template pt ON (p.product_tmpl_id=pt.id) LEFT JOIN uom_uom u ON (u.id=pt.uom_id) LEFT JOIN pos_session ps ON (s.session_id=ps.id) """ def _group_by(self): return """ GROUP BY s.id, s.date_order, s.partner_id,s.state, pt.categ_id, s.user_id, s.location_id, s.company_id, s.sale_journal, s.pricelist_id, s.invoice_id, s.create_date, s.session_id, l.product_id, pt.categ_id, pt.pos_categ_id, p.product_tmpl_id, ps.config_id """ def _having(self): return """ HAVING SUM(l.qty * u.factor) != 0 """ @api.model_cr def init(self): tools.drop_view_if_exists(self._cr, self._table) self._cr.execute(""" CREATE OR REPLACE VIEW %s AS ( %s %s %s %s ) """ % (self._table, self._select(), self._from(), self._group_by(), self._having()))
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 a 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'] _description = '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', readonly=False) notification_type = fields.Selection( [('email', 'Handle by Emails'), ('inbox', 'Handle in Eagle')], 'Notification Management', required=True, default='email', help="Policy on how to handle Chatter notifications:\n" "- Handle by Emails: notifications are sent to your email address\n" "- Handle in Eagle: notifications appear in your Eagle Inbox") # channel-specific: moderation is_moderator = fields.Boolean(string='Is moderator', compute='_compute_is_moderator') moderation_counter = fields.Integer(string='Moderation count', compute='_compute_moderation_counter') moderation_channel_ids = fields.Many2many('mail.channel', 'mail_channel_moderator_rel', string='Moderated channels') @api.depends('moderation_channel_ids.moderation', 'moderation_channel_ids.moderator_ids') @api.multi def _compute_is_moderator(self): moderated = self.env['mail.channel'].search([ ('id', 'in', self.mapped('moderation_channel_ids').ids), ('moderation', '=', True), ('moderator_ids', 'in', self.ids) ]) user_ids = moderated.mapped('moderator_ids') for user in self: user.is_moderator = user in user_ids @api.multi def _compute_moderation_counter(self): self._cr.execute( """ SELECT channel_moderator.res_users_id, COUNT(msg.id) FROM "mail_channel_moderator_rel" AS channel_moderator JOIN "mail_message" AS msg ON channel_moderator.mail_channel_id = msg.res_id AND channel_moderator.res_users_id IN %s AND msg.model = 'mail.channel' AND msg.moderation_status = 'pending_moderation' GROUP BY channel_moderator.res_users_id""", [tuple(self.ids)]) result = dict(self._cr.fetchall()) for user in self: user.moderation_counter = result.get(user.id, 0) 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) # Auto-subscribe to channels self.env['mail.channel'].search([ ('group_ids', 'in', user.groups_id.ids) ])._subscribe_users() return user @api.multi def write(self, vals): write_res = super(Users, self).write(vals) sel_groups = [ vals[k] for k in vals if is_selection_groups(k) and vals[k] ] 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() elif sel_groups: self.env['mail.channel'].search([('group_ids', 'in', sel_groups) ])._subscribe_users() return write_res @api.model def systray_get_activities(self): query = """SELECT m.id, count(*), act.res_model as model, CASE WHEN %(today)s::date - act.date_deadline::date = 0 Then 'today' WHEN %(today)s::date - act.date_deadline::date > 0 Then 'overdue' WHEN %(today)s::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 = %(user_id)s GROUP BY m.id, states, act.res_model; """ self.env.cr.execute(query, { 'today': fields.Date.context_today(self), 'user_id': self.env.uid, }) activity_data = self.env.cr.dictfetchall() model_ids = [a['id'] for a in activity_data] model_names = { n[0]: n[1] for n in self.env['ir.model'].browse(model_ids).name_get() } user_activities = {} for activity in activity_data: if not user_activities.get(activity['model']): user_activities[activity['model']] = { 'name': model_names[activity['id']], 'model': activity['model'], 'type': 'activity', '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 res_partner_has_image(models.Model): _inherit = "res.partner" @api.multi def _has_image(self): return dict((p.id, bool(p.image)) for p in self) has_image = fields.Boolean(compute='_has_image', string="Image Res") property_account_customer_advance = fields.Many2one( 'account.account', string="Account Customer Advance", help="This account will be used for advance payment of custom") @api.multi def _get_re_reg_advance_account(self): re_reg_account_rec = self.env['account.account'].search([('code', '=', '210602')]) for rec in self: if rec.is_student or rec.is_parent: if re_reg_account_rec.id: rec.re_reg_advance_account = re_reg_account_rec.id else: rec.re_reg_advance_account = False # @api.depends('advance_total_recivable') # def get_advance_total_recivable(self): # """ # ----------------------------------------------------- # :return: # """ # # account_move_line_obj = self.env['account.move.line'] # query = account_move_line_obj._query_get_get() # for record in self: # if record.property_account_customer_advance.id: # amount_difference = 0.00 # # for account_move_line_rec in account_move_line_obj.search([('partner_id','=',record.id)]): # # if account_move_line_rec.account_id.id == record.property_account_customer_advance.id: # # amount_difference += account_move_line_rec.credit # # amount_difference -= account_move_line_rec.debit # # record.re_reg_total_recivable = amount_difference # ctx = self._context.copy() # ctx['all_fiscalyear'] = True # query = self.env['account.move.line']._query_get_get() # self._cr.execute("""SELECT l.partner_id, SUM(l.debit),SUM(l.credit), SUM(l.debit-l.credit) # FROM account_move_line l # WHERE l.partner_id IN %s # AND l.account_id IN %s # AND l.reconcile_id IS NULL # AND """ + query + """ # GROUP BY l.partner_id # """, (tuple(record.ids), tuple(record.property_account_customer_advance.ids),)) # fetch = self._cr.fetchall() # for pid, total_debit, total_credit, val in fetch: # amount_difference += total_credit # amount_difference -= total_debit # self.advance_total_recivable = amount_difference @api.depends('re_reg_total_recivable') def get_re_registration_total_recivable(self): """ ----------------------------------------------------- :return: """ account_move_line_obj = self.env['account.move.line'] query = account_move_line_obj._query_get() for record in self: if record.re_reg_advance_account.id: amount_difference = 0.00 # for account_move_line_rec in account_move_line_obj.search([('partner_id','=',record.id)]): # if account_move_line_rec.account_id.id == record.property_account_customer_advance.id: # amount_difference += account_move_line_rec.credit # amount_difference -= account_move_line_rec.debit # record.re_reg_total_recivable = amount_difference ctx = self._context.copy() ctx['all_fiscalyear'] = True query = self.env['account.move.line']._query_get() self._cr.execute( """SELECT l.partner_id, SUM(l.debit),SUM(l.credit), SUM(l.debit-l.credit) FROM account_move_line l WHERE l.partner_id IN %s AND l.account_id IN %s AND l.reconcile_id IS NULL AND """ + query + """ GROUP BY l.partner_id """, ( tuple(record.ids), tuple(record.re_reg_advance_account.ids), )) fetch = self._cr.fetchall() for pid, total_debit, total_credit, val in fetch: amount_difference += total_credit amount_difference -= total_debit self.re_reg_total_recivable = amount_difference @api.one @api.depends('tc_initiated') def _get_tc_initiad(self): obj_transfer_certificate = self.env['trensfer.certificate'] for rec in self: if rec.is_student == True: if rec.id: tc_rec = obj_transfer_certificate.search( [('name', '=', rec.id)], limit=1) if tc_rec.id: if tc_rec.state in ('tc_requested', 'fee_balance_review', 'final_fee_awaited'): rec.tc_initiated = 'yes' if tc_rec.state in ('tc_complete', 'tc_cancel'): rec.tc_initiated = 'no' else: rec.tc_initiated = 'no' student_state = fields.Selection( [('academic_fee_paid', 'Academic fee paid'), ('academic_fee_unpaid', 'Academic fee unpaid'), ('academic_fee_partially_paid', 'Academic fee partially paid'), ('tc_initiated', 'TC initiated'), ('confirmed_student', 'Confirmed Student'), ('ministry_approved_old', 'Ministry Approved')], string='Status') tc_initiated = fields.Selection([('yes', 'Yes'), ('no', 'No')], string='TC Initiated', compute='_get_tc_initiad') re_reg_next_academic_year = fields.Selection( [('yes', 'YES'), ('no', 'NO')], 'Re-registered for next Academic year', default='no') re_reg_advance_account = fields.Many2one( 'account.account', string="Account Re-Registration Advance", help= "This account will be used for Re-Registration fee advance payment of Student/Parent", compute=_get_re_reg_advance_account) re_reg_total_recivable = fields.Float( compute='get_re_registration_total_recivable', string='Re-Reg Advance Total Recivable') advance_total_recivable = fields.Float( compute='get_advance_total_recivable', string='Advance Total Recivable')
class Partner(models.Model): _inherit = 'res.partner' associate_member = fields.Many2one( 'res.partner', string='Associate Member', help="A member with whom you want to associate your membership." "It will consider the membership state of the associated member.") member_lines = fields.One2many('membership.membership_line', 'partner', string='Membership') free_member = fields.Boolean( string='Free Member', help="Select if you want to give free membership.") membership_amount = fields.Float( string='Membership Amount', digits=(16, 2), help='The price negotiated by the partner') membership_state = fields.Selection( membership.STATE, compute='_compute_membership_state', string='Current Membership Status', store=True, help='It indicates the membership state.\n' '-Non Member: A partner who has not applied for any membership.\n' '-Cancelled Member: A member who has cancelled his membership.\n' '-Old Member: A member whose membership date has expired.\n' '-Waiting Member: A member who has applied for the membership and whose invoice is going to be created.\n' '-Invoiced Member: A member whose invoice has been created.\n' '-Paying member: A member who has paid the membership fee.') membership_start = fields.Date( compute='_compute_membership_start', string='Membership Start Date', store=True, help="Date from which membership becomes active.") membership_stop = fields.Date( compute='_compute_membership_stop', string='Membership End Date', store=True, help="Date until which membership remains active.") membership_cancel = fields.Date( compute='_compute_membership_cancel', string='Cancel Membership Date', store=True, help="Date on which membership has been cancelled") @api.depends( 'member_lines.account_invoice_line.invoice_id.state', 'member_lines.account_invoice_line.invoice_id.invoice_line_ids', 'member_lines.account_invoice_line.invoice_id.payment_ids', 'member_lines.account_invoice_line.invoice_id.payment_move_line_ids', 'member_lines.account_invoice_line.invoice_id.partner_id', 'free_member', 'member_lines.date_to', 'member_lines.date_from', 'associate_member') def _compute_membership_state(self): values = self._membership_state() for partner in self: partner.membership_state = values[partner.id] # Do not depend directly on "associate_member.membership_state" or we might end up in an # infinite loop. Since we still need this dependency somehow, we explicitly search for the # "parent members" and trigger a recompute. parent_members = self.search([('associate_member', 'in', self.ids) ]) - self if parent_members: parent_members._recompute_todo(self._fields['membership_state']) @api.depends( 'member_lines.account_invoice_line.invoice_id.state', 'member_lines.account_invoice_line.invoice_id.invoice_line_ids', 'member_lines.account_invoice_line.invoice_id.payment_ids', 'free_member', 'member_lines.date_to', 'member_lines.date_from', 'member_lines.date_cancel', 'membership_state', 'associate_member.membership_state') def _compute_membership_start(self): """Return date of membership""" for partner in self: partner.membership_start = self.env[ 'membership.membership_line'].search( [('partner', '=', partner.associate_member.id or partner.id), ('date_cancel', '=', False)], limit=1, order='date_from').date_from @api.depends( 'member_lines.account_invoice_line.invoice_id.state', 'member_lines.account_invoice_line.invoice_id.invoice_line_ids', 'member_lines.account_invoice_line.invoice_id.payment_ids', 'free_member', 'member_lines.date_to', 'member_lines.date_from', 'member_lines.date_cancel', 'membership_state', 'associate_member.membership_state') def _compute_membership_stop(self): MemberLine = self.env['membership.membership_line'] for partner in self: partner.membership_stop = self.env[ 'membership.membership_line'].search( [('partner', '=', partner.associate_member.id or partner.id), ('date_cancel', '=', False)], limit=1, order='date_to desc').date_to @api.depends( 'member_lines.account_invoice_line.invoice_id.state', 'member_lines.account_invoice_line.invoice_id.invoice_line_ids', 'member_lines.account_invoice_line.invoice_id.payment_ids', 'free_member', 'member_lines.date_to', 'member_lines.date_from', 'member_lines.date_cancel', 'membership_state', 'associate_member.membership_state') def _compute_membership_cancel(self): for partner in self: if partner.membership_state == 'canceled': partner.membership_cancel = self.env[ 'membership.membership_line'].search( [('partner', '=', partner.id)], limit=1, order='date_cancel').date_cancel else: partner.membership_cancel = False def _membership_state(self): """This Function return Membership State For Given Partner. """ res = {} today = fields.Date.today() for partner in self: res[partner.id] = 'none' if partner.membership_cancel and today > partner.membership_cancel: res[partner.id] = 'free' if partner.free_member else 'canceled' continue if partner.membership_stop and today > partner.membership_stop: res[partner.id] = 'free' if partner.free_member else 'old' continue if partner.associate_member: res_state = partner.associate_member._membership_state() res[partner.id] = res_state[partner.associate_member.id] continue s = 4 if partner.member_lines: for mline in partner.member_lines: if (mline.date_to or date.min) >= today and ( mline.date_from or date.min) <= today: if mline.account_invoice_line.invoice_id.partner_id == partner: mstate = mline.account_invoice_line.invoice_id.state if mstate == 'paid': s = 0 inv = mline.account_invoice_line.invoice_id for ml in inv.payment_move_line_ids: if any( ml.invoice_id.filtered( lambda inv: inv.type == 'out_refund')): s = 2 break elif mstate == 'open' and s != 0: s = 1 elif mstate == 'cancel' and s != 0 and s != 1: s = 2 elif mstate == 'draft' and s != 0 and s != 1: s = 3 if s == 4: for mline in partner.member_lines: if (mline.date_from or date.min) < today and ( mline.date_to or date.min ) < today and (mline.date_from or date.min) <= ( mline.date_to or date.min ) and mline.account_invoice_line and mline.account_invoice_line.invoice_id.state == 'paid': s = 5 else: s = 6 if s == 0: res[partner.id] = 'paid' elif s == 1: res[partner.id] = 'invoiced' elif s == 2: res[partner.id] = 'canceled' elif s == 3: res[partner.id] = 'waiting' elif s == 5: res[partner.id] = 'old' elif s == 6: res[partner.id] = 'none' if partner.free_member and s != 0: res[partner.id] = 'free' return res @api.one @api.constrains('associate_member') def _check_recursion_associate_member(self): level = 100 while self: self = self.associate_member if not level: raise ValidationError( _('You cannot create recursive associated members.')) level -= 1 @api.model def _cron_update_membership(self): partners = self.search([('membership_state', 'in', ['invoiced', 'paid'])]) # mark the field to be recomputed, and recompute it partners._recompute_todo(self._fields['membership_state']) self.recompute() @api.multi def create_membership_invoice(self, product_id=None, datas=None): """ Create Customer Invoice of Membership for partners. @param datas: datas has dictionary value which consist Id of Membership product and Cost Amount of Membership. datas = {'membership_product_id': None, 'amount': None} """ product_id = product_id or datas.get('membership_product_id') amount = datas.get('amount', 0.0) invoice_list = [] for partner in self: addr = partner.address_get(['invoice']) if partner.free_member: raise UserError(_("Partner is a free Member.")) if not addr.get('invoice', False): raise UserError( _("Partner doesn't have an address to make the invoice.")) invoice = self.env['account.invoice'].create({ 'partner_id': partner.id, 'account_id': partner.property_account_receivable_id.id, 'fiscal_position_id': partner.property_account_position_id.id }) line_values = { 'product_id': product_id, 'price_unit': amount, 'invoice_id': invoice.id, } # create a record in cache, apply onchange then revert back to a dictionnary invoice_line = self.env['account.invoice.line'].new(line_values) invoice_line._onchange_product_id() line_values = invoice_line._convert_to_write( {name: invoice_line[name] for name in invoice_line._cache}) line_values['price_unit'] = amount invoice.write({'invoice_line_ids': [(0, 0, line_values)]}) invoice_list.append(invoice.id) invoice.compute_taxes() return invoice_list
class Company(models.Model): _inherit = "res.company" invoice_is_snailmail = fields.Boolean(string='Send by Post', default=False)
class StockWarehouse(models.Model): _inherit = 'stock.warehouse' manufacture_to_resupply = fields.Boolean( 'Manufacture to Resupply', default=True, help= "When products are manufactured, they can be manufactured in this warehouse." ) manufacture_pull_id = fields.Many2one('stock.rule', 'Manufacture Rule') manufacture_mto_pull_id = fields.Many2one('stock.rule', 'Manufacture MTO Rule') pbm_mto_pull_id = fields.Many2one('stock.rule', 'Picking Before Manufacturing MTO Rule') sam_rule_id = fields.Many2one('stock.rule', 'Stock After Manufacturing Rule') manu_type_id = fields.Many2one( 'stock.picking.type', 'Manufacturing Operation Type', domain= "[('code', '=', 'mrp_operation'), ('company_id', '=', company_id)]", check_company=True) pbm_type_id = fields.Many2one( 'stock.picking.type', 'Picking Before Manufacturing Operation Type', check_company=True) sam_type_id = fields.Many2one('stock.picking.type', 'Stock After Manufacturing Operation Type', check_company=True) manufacture_steps = fields.Selection( [('mrp_one_step', 'Manufacture (1 step)'), ('pbm', 'Pick components and then manufacture (2 steps)'), ('pbm_sam', 'Pick components, manufacture and then store products (3 steps)')], 'Manufacture', default='mrp_one_step', required=True, help="Produce : Move the components to the production location\ directly and start the manufacturing process.\nPick / Produce : Unload\ the components from the Stock to Input location first, and then\ transfer it to the Production location.") pbm_route_id = fields.Many2one('stock.location.route', 'Picking Before Manufacturing Route', ondelete='restrict') pbm_loc_id = fields.Many2one('stock.location', 'Picking before Manufacturing Location', check_company=True) sam_loc_id = fields.Many2one('stock.location', 'Stock after Manufacturing Location', check_company=True) def get_rules_dict(self): result = super(StockWarehouse, self).get_rules_dict() production_location_id = self._get_production_location() for warehouse in self: result[warehouse.id].update({ 'mrp_one_step': [], 'pbm': [ self.Routing(warehouse.lot_stock_id, warehouse.pbm_loc_id, warehouse.pbm_type_id, 'pull'), self.Routing(warehouse.pbm_loc_id, production_location_id, warehouse.manu_type_id, 'pull'), ], 'pbm_sam': [ self.Routing(warehouse.lot_stock_id, warehouse.pbm_loc_id, warehouse.pbm_type_id, 'pull'), self.Routing(warehouse.pbm_loc_id, production_location_id, warehouse.manu_type_id, 'pull'), self.Routing(warehouse.sam_loc_id, warehouse.lot_stock_id, warehouse.sam_type_id, 'push'), ], }) return result @api.model def _get_production_location(self): location = self.env['stock.location'].with_context( force_company=self.company_id.id).search( [('usage', '=', 'production'), ('company_id', '=', self.company_id.id)], limit=1) if not location: raise UserError(_('Can\'t find any production location.')) return location def _get_routes_values(self): routes = super(StockWarehouse, self)._get_routes_values() routes.update({ 'pbm_route_id': { 'routing_key': self.manufacture_steps, 'depends': ['manufacture_steps', 'manufacture_to_resupply'], 'route_update_values': { 'name': self._format_routename(route_type=self.manufacture_steps), 'active': self.manufacture_steps != 'mrp_one_step', }, 'route_create_values': { 'product_categ_selectable': True, 'warehouse_selectable': True, 'product_selectable': False, 'company_id': self.company_id.id, 'sequence': 10, }, 'rules_values': { 'active': True, } } }) return routes def _get_route_name(self, route_type): names = { 'mrp_one_step': _('Manufacture (1 step)'), 'pbm': _('Pick components and then manufacture'), 'pbm_sam': _('Pick components, manufacture and then store products (3 steps)' ), } if route_type in names: return names[route_type] else: return super(StockWarehouse, self)._get_route_name(route_type) def _get_global_route_rules_values(self): rules = super(StockWarehouse, self)._get_global_route_rules_values() location_src = self.manufacture_steps == 'mrp_one_step' and self.lot_stock_id or self.pbm_loc_id production_location = self._get_production_location() location_id = self.manufacture_steps == 'pbm_sam' and self.sam_loc_id or self.lot_stock_id rules.update({ 'manufacture_pull_id': { 'depends': ['manufacture_steps', 'manufacture_to_resupply'], 'create_values': { 'action': 'manufacture', 'procure_method': 'make_to_order', 'company_id': self.company_id.id, 'picking_type_id': self.manu_type_id.id, 'route_id': self._find_global_route('mrp.route_warehouse0_manufacture', _('Manufacture')).id }, 'update_values': { 'active': self.manufacture_to_resupply, 'name': self._format_rulename(location_id, False, 'Production'), 'location_id': location_id.id, 'propagate_cancel': self.manufacture_steps == 'pbm_sam' }, }, 'manufacture_mto_pull_id': { 'depends': ['manufacture_steps', 'manufacture_to_resupply'], 'create_values': { 'procure_method': 'make_to_order', 'company_id': self.company_id.id, 'action': 'pull', 'auto': 'manual', 'route_id': self._find_global_route('stock.route_warehouse0_mto', _('Make To Order')).id, 'location_id': production_location.id, 'location_src_id': location_src.id, 'picking_type_id': self.manu_type_id.id }, 'update_values': { 'name': self._format_rulename(location_src, production_location, 'MTO'), 'active': self.manufacture_to_resupply, }, }, 'pbm_mto_pull_id': { 'depends': ['manufacture_steps', 'manufacture_to_resupply'], 'create_values': { 'procure_method': 'make_to_order', 'company_id': self.company_id.id, 'action': 'pull', 'auto': 'manual', 'route_id': self._find_global_route('stock.route_warehouse0_mto', _('Make To Order')).id, 'name': self._format_rulename(self.lot_stock_id, self.pbm_loc_id, 'MTO'), 'location_id': self.pbm_loc_id.id, 'location_src_id': self.lot_stock_id.id, 'picking_type_id': self.pbm_type_id.id }, 'update_values': { 'active': self.manufacture_steps != 'mrp_one_step' and self.manufacture_to_resupply, } }, # The purpose to move sam rule in the manufacture route instead of # pbm_route_id is to avoid conflict with receipt in multiple # step. For example if the product is manufacture and receipt in two # step it would conflict in WH/Stock since product could come from # WH/post-prod or WH/input. We do not have this conflict with # manufacture route since it is set on the product. 'sam_rule_id': { 'depends': ['manufacture_steps', 'manufacture_to_resupply'], 'create_values': { 'procure_method': 'make_to_order', 'company_id': self.company_id.id, 'action': 'pull', 'auto': 'manual', 'route_id': self._find_global_route('mrp.route_warehouse0_manufacture', _('Manufacture')).id, 'name': self._format_rulename(self.sam_loc_id, self.lot_stock_id, False), 'location_id': self.lot_stock_id.id, 'location_src_id': self.sam_loc_id.id, 'picking_type_id': self.sam_type_id.id }, 'update_values': { 'active': self.manufacture_steps == 'pbm_sam' and self.manufacture_to_resupply, } } }) return rules def _get_locations_values(self, vals, code=False): values = super(StockWarehouse, self)._get_locations_values(vals, code=code) def_values = self.default_get(['manufacture_steps']) manufacture_steps = vals.get('manufacture_steps', def_values['manufacture_steps']) code = vals.get('code') or code or '' code = code.replace(' ', '').upper() company_id = vals.get('company_id', self.company_id.id) values.update({ 'pbm_loc_id': { 'name': _('Pre-Production'), 'active': manufacture_steps in ('pbm', 'pbm_sam'), 'usage': 'internal', 'barcode': self._valid_barcode(code + '-PREPRODUCTION', company_id) }, 'sam_loc_id': { 'name': _('Post-Production'), 'active': manufacture_steps == 'pbm_sam', 'usage': 'internal', 'barcode': self._valid_barcode(code + '-POSTPRODUCTION', company_id) }, }) return values def _get_sequence_values(self): values = super(StockWarehouse, self)._get_sequence_values() values.update({ 'pbm_type_id': { 'name': self.name + ' ' + _('Sequence picking before manufacturing'), 'prefix': self.code + '/PC/', 'padding': 5, 'company_id': self.company_id.id }, 'sam_type_id': { 'name': self.name + ' ' + _('Sequence stock after manufacturing'), 'prefix': self.code + '/SFP/', 'padding': 5, 'company_id': self.company_id.id }, 'manu_type_id': { 'name': self.name + ' ' + _('Sequence production'), 'prefix': self.code + '/MO/', 'padding': 5, 'company_id': self.company_id.id }, }) return values def _get_picking_type_create_values(self, max_sequence): data, next_sequence = super( StockWarehouse, self)._get_picking_type_create_values(max_sequence) data.update({ 'pbm_type_id': { 'name': _('Pick Components'), 'code': 'internal', 'use_create_lots': True, 'use_existing_lots': True, 'default_location_src_id': self.lot_stock_id.id, 'default_location_dest_id': self.pbm_loc_id.id, 'sequence': next_sequence + 1, 'sequence_code': 'PC', 'company_id': self.company_id.id, }, 'sam_type_id': { 'name': _('Store Finished Product'), 'code': 'internal', 'use_create_lots': True, 'use_existing_lots': True, 'default_location_src_id': self.sam_loc_id.id, 'default_location_dest_id': self.lot_stock_id.id, 'sequence': next_sequence + 3, 'sequence_code': 'SFP', 'company_id': self.company_id.id, }, 'manu_type_id': { 'name': _('Manufacturing'), 'code': 'mrp_operation', 'use_create_lots': True, 'use_existing_lots': True, 'sequence': next_sequence + 2, 'sequence_code': 'MO', 'company_id': self.company_id.id, }, }) return data, max_sequence + 4 def _get_picking_type_update_values(self): data = super(StockWarehouse, self)._get_picking_type_update_values() data.update({ 'pbm_type_id': { 'active': self.manufacture_to_resupply and self.manufacture_steps in ('pbm', 'pbm_sam') }, 'sam_type_id': { 'active': self.manufacture_to_resupply and self.manufacture_steps == 'pbm_sam' }, 'manu_type_id': { 'active': self.manufacture_to_resupply, 'default_location_src_id': self.manufacture_steps in ('pbm', 'pbm_sam') and self.pbm_loc_id.id or self.lot_stock_id.id, 'default_location_dest_id': self.manufacture_steps == 'pbm_sam' and self.sam_loc_id.id or self.lot_stock_id.id, }, }) return data def write(self, vals): if any(field in vals for field in ('manufacture_steps', 'manufacture_to_resupply')): for warehouse in self: warehouse._update_location_manufacture( vals.get('manufacture_steps', warehouse.manufacture_steps)) return super(StockWarehouse, self).write(vals) def _get_all_routes(self): routes = super(StockWarehouse, self)._get_all_routes() routes |= self.filtered( lambda self: self.manufacture_to_resupply and self. manufacture_pull_id and self.manufacture_pull_id.route_id).mapped( 'manufacture_pull_id').mapped('route_id') return routes def _update_location_manufacture(self, new_manufacture_step): self.mapped('pbm_loc_id').write( {'active': new_manufacture_step != 'mrp_one_step'}) self.mapped('sam_loc_id').write( {'active': new_manufacture_step == 'pbm_sam'}) def _update_name_and_code(self, name=False, code=False): res = super(StockWarehouse, self)._update_name_and_code(name, code) # change the manufacture stock rule name for warehouse in self: if warehouse.manufacture_pull_id and name: warehouse.manufacture_pull_id.write({ 'name': warehouse.manufacture_pull_id.name.replace( warehouse.name, name, 1) }) return res
class MailResendMessage(models.TransientModel): _name = 'mail.resend.message' _description = 'Email resend wizard' mail_message_id = fields.Many2one('mail.message', 'Message', readonly=True) partner_ids = fields.One2many('mail.resend.partner', 'resend_wizard_id', string='Recipients') notification_ids = fields.Many2many('mail.notification', string='Notifications', readonly=True) has_cancel = fields.Boolean(compute='_compute_has_cancel') partner_readonly = fields.Boolean(compute='_compute_partner_readonly') @api.depends("partner_ids") def _compute_has_cancel(self): self.has_cancel = self.partner_ids.filtered(lambda p: not p.resend) def _compute_partner_readonly(self): self.partner_readonly = not self.env['res.partner'].check_access_rights('write', raise_exception=False) @api.model def default_get(self, fields): rec = super(MailResendMessage, self).default_get(fields) message_id = self._context.get('mail_message_to_resend') if message_id: mail_message_id = self.env['mail.message'].browse(message_id) notification_ids = mail_message_id.notification_ids.filtered(lambda notif: notif.notification_type == 'email' and notif.notification_status in ('exception', 'bounce')) partner_ids = [(0, 0, { "partner_id": notif.res_partner_id.id, "name": notif.res_partner_id.name, "email": notif.res_partner_id.email, "resend": True, "message": notif.format_failure_reason(), }) for notif in notification_ids] has_user = any([notif.res_partner_id.user_ids for notif in notification_ids]) if has_user: partner_readonly = not self.env['res.users'].check_access_rights('write', raise_exception=False) else: partner_readonly = not self.env['res.partner'].check_access_rights('write', raise_exception=False) rec['partner_readonly'] = partner_readonly rec['notification_ids'] = [(6, 0, notification_ids.ids)] rec['mail_message_id'] = mail_message_id.id rec['partner_ids'] = partner_ids else: raise UserError(_('No message_id found in context')) return rec def resend_mail_action(self): """ Process the wizard content and proceed with sending the related email(s), rendering any template patterns on the fly if needed. """ for wizard in self: "If a partner disappeared from partner list, we cancel the notification" to_cancel = wizard.partner_ids.filtered(lambda p: not p.resend).mapped("partner_id") to_send = wizard.partner_ids.filtered(lambda p: p.resend).mapped("partner_id") notif_to_cancel = wizard.notification_ids.filtered(lambda notif: notif.notification_type == 'email' and notif.res_partner_id in to_cancel and notif.notification_status in ('exception', 'bounce')) notif_to_cancel.sudo().write({'notification_status': 'canceled'}) if to_send: message = wizard.mail_message_id record = self.env[message.model].browse(message.res_id) if message.is_thread_message() else self.env['mail.thread'] email_partners_data = [] for pid, cid, active, pshare, ctype, notif, groups in self.env['mail.followers']._get_recipient_data(None, 'comment', False, pids=to_send.ids): if pid and notif == 'email' or not notif: pdata = {'id': pid, 'share': pshare, 'active': active, 'notif': 'email', 'groups': groups or []} if not pshare and notif: # has an user and is not shared, is therefore user email_partners_data.append(dict(pdata, type='user')) elif pshare and notif: # has an user and is shared, is therefore portal email_partners_data.append(dict(pdata, type='portal')) else: # has no user, is therefore customer email_partners_data.append(dict(pdata, type='customer')) record._notify_record_by_email(message, {'partners': email_partners_data}, check_existing=True, send_after_commit=False) self.mail_message_id._notify_mail_failure_update() return {'type': 'ir.actions.act_window_close'} def cancel_mail_action(self): for wizard in self: for notif in wizard.notification_ids: notif.filtered(lambda notif: notif.notification_type == 'email' and notif.notification_status in ('exception', 'bounce')).sudo().write({'notification_status': 'canceled'}) wizard.mail_message_id._notify_mail_failure_update() return {'type': 'ir.actions.act_window_close'}
class EagleeduAssigningClass(models.Model): _name = 'eagleedu.assigning.class' _description = 'Assign the Students to Class' _inherit = ['mail.thread'] # _rec_name = 'class_assign_name' name = fields.Char('Class Assign Register', compute='get_class_assign_name') keep_roll_no = fields.Boolean("keep Roll No") class_id = fields.Many2one('eagleedu.class', string='Class') student_list = fields.One2many('eagleedu.student.list', 'connect_id', string="Students") admitted_class = fields.Many2one('eagleedu.class.division', string="Admitted Class") assigned_by = fields.Many2one('res.users', string='Assigned By', default=lambda self: self.env.uid) state = fields.Selection([('draft', 'Draft'), ('done', 'Done')], string='State', required=True, default='draft', track_visibility='onchange') assign_date = fields.Datetime(string='Asigned Date', default=datetime.today()) #assign_date = fields.Date(string='Assigned Date',default=fields.date.today()) @api.model def get_class_assign_name(self): for rec in self: rec.name = str(rec.admitted_class.name) + '(Assign on ' + str( rec.assign_date) + ')' #rec.name = rec.admitted_class.name #+ '(assigned on '+ rec.assign_date +')' def assigning_class(self): max_roll = self.env['eagleedu.class.history'].search( [('class_id', '=', self.admitted_class.id)], order='roll_no desc', limit=1) if max_roll.roll_no: next_roll = max_roll.roll_no else: next_roll = 0 for rec in self: if not self.student_list: raise ValidationError(_('No Student Lines')) com_sub = self.env['eagleedu.syllabus'].search([ ('class_id', '=', rec.class_id.id), ('academic_year', '=', rec.admitted_class.academic_year_id.id), ('divisional', '=', False), ('selection_type', '=', 'compulsory') ]) elect_sub = self.env['eagleedu.syllabus'].search([ ('class_id', '=', rec.class_id.id), ('academic_year', '=', rec.admitted_class.academic_year_id.id), ('divisional', '=', True), ('division_id', '=', rec.admitted_class.id), ('selection_type', '=', 'compulsory') ]) com_subjects = [] # compulsory Subject List el_subjects = [] # Elective Subject List for sub in com_sub: com_subjects.append(sub.id) for sub in elect_sub: el_subjects.append(sub.id) for line in self.student_list: st = self.env['eagleedu.student'].search([ ('id', '=', line.student_id.id) ]) st.class_id = rec.admitted_class.id if self.keep_roll_no != True: next_roll = next_roll + 1 line.roll_no = next_roll st.roll_no = line.roll_no # create student history self.env['eagleedu.class.history'].create({ 'academic_year_id': rec.admitted_class.academic_year_id.id, 'class_id': rec.admitted_class.id, 'student_id': line.student_id.id, 'roll_no': line.roll_no, 'compulsory_subjects': [(6, 0, com_subjects)], 'selective_subjects': [(6, 0, el_subjects)] }) self.write({'state': 'done'}) def unlink(self): """Return warning if the Record is in done state""" for rec in self: if rec.state == 'done': raise ValidationError(_("Cannot delete Record in Done state")) def get_student_list(self): """returns the list of students applied to join the selected class""" for rec in self: for line in rec.student_list: line.unlink() # TODO apply filter not to get student assigned previously students = self.env['eagleedu.student'].search([ ('class_id', '=', rec.admitted_class.id), ('assigned', '=', False) ]) if not students: raise ValidationError(_('No Students Available.. !')) values = [] for stud in students: stud_line = { 'class_id': rec.class_id.id, 'student_id': stud.id, 'connect_id': rec.id, 'roll_no': stud.application_id.roll_no } stud.assigned = True values.append(stud_line) for line in values: rec.student_line = self.env['eagleedu.student.list'].create( line)
class PaymentAdviceReport(models.Model): _name = "payment.advice.report" _description = "Payment Advice Analysis" _auto = False name = fields.Char(readonly=True) date = fields.Date(readonly=True) year = fields.Char(readonly=True) month = fields.Selection([('01', 'January'), ('02', 'February'), ('03', 'March'), ('04', 'April'), ('05', 'May'), ('06', 'June'), ('07', 'July'), ('08', 'August'), ('09', 'September'), ('10', 'October'), ('11', 'November'), ('12', 'December')], readonly=True) day = fields.Char(readonly=True) state = fields.Selection([ ('draft', 'Draft'), ('confirm', 'Confirmed'), ('cancel', 'Cancelled'), ], string='Status', index=True, readonly=True) employee_id = fields.Many2one('hr.employee', string='Employee', readonly=True) nbr = fields.Integer(string='# Payment Lines', readonly=True) number = fields.Char(readonly=True) bysal = fields.Float(string='By Salary', readonly=True) bank_id = fields.Many2one('res.bank', string='Bank', readonly=True) company_id = fields.Many2one('res.company', string='Company', readonly=True) cheque_nos = fields.Char(string='Cheque Numbers', readonly=True) neft = fields.Boolean(string='NEFT Transaction', readonly=True) ifsc_code = fields.Char(string='IFSC Code', readonly=True) employee_bank_no = fields.Char(string='Employee Bank Account', required=True) @api.model_cr def init(self): drop_view_if_exists(self.env.cr, self._table) self.env.cr.execute(""" create or replace view payment_advice_report as ( select min(l.id) as id, sum(l.bysal) as bysal, p.name, p.state, p.date, p.number, p.company_id, p.bank_id, p.chaque_nos as cheque_nos, p.neft, l.employee_id, l.ifsc_code, l.name as employee_bank_no, to_char(p.date, 'YYYY') as year, to_char(p.date, 'MM') as month, to_char(p.date, 'YYYY-MM-DD') as day, 1 as nbr from hr_payroll_advice as p left join hr_payroll_advice_line as l on (p.id=l.advice_id) where l.employee_id IS NOT NULL group by p.number,p.name,p.date,p.state,p.company_id,p.bank_id,p.chaque_nos,p.neft, l.employee_id,l.advice_id,l.bysal,l.ifsc_code, l.name ) """)
class PosSession(models.Model): _name = 'pos.session' _order = 'id desc' _description = 'Point of Sale Session' POS_SESSION_STATE = [ ('opening_control', 'Opening Control'), # method action_pos_session_open ('opened', 'In Progress'), # method action_pos_session_closing_control ('closing_control', 'Closing Control'), # method action_pos_session_close ('closed', 'Closed & Posted'), ] def _confirm_orders(self): for session in self: company_id = session.config_id.journal_id.company_id.id orders = session.order_ids.filtered( lambda order: order.state == 'paid') journal_id = self.env['ir.config_parameter'].sudo().get_param( 'pos.closing.journal_id_%s' % company_id, default=session.config_id.journal_id.id) if not journal_id: raise UserError( _("You have to set a Sale Journal for the POS:%s") % (session.config_id.name, )) move = self.env['pos.order'].with_context( force_company=company_id)._create_account_move( session.start_at, session.name, int(journal_id), company_id) orders.with_context( force_company=company_id)._create_account_move_line( session, move) for order in session.order_ids.filtered( lambda o: o.state not in ['done', 'invoiced']): if order.state not in ('paid'): raise UserError( _("You cannot confirm all orders of this session, because they have not the 'paid' status.\n" "{reference} is in state {state}, total amount: {total}, paid: {paid}" ).format( reference=order.pos_reference or order.name, state=order.state, total=order.amount_total, paid=order.amount_paid, )) order.action_pos_order_done() orders_to_reconcile = session.order_ids._filtered_for_reconciliation( ) orders_to_reconcile.sudo()._reconcile_payments() config_id = fields.Many2one( 'pos.config', string='Point of Sale', help="The physical point of sale you will use.", required=True, index=True) name = fields.Char(string='Session ID', required=True, readonly=True, default='/') user_id = fields.Many2one( 'res.users', string='Responsible', required=True, index=True, readonly=True, states={'opening_control': [('readonly', False)]}, default=lambda self: self.env.uid) currency_id = fields.Many2one('res.currency', related='config_id.currency_id', string="Currency", readonly=False) start_at = fields.Datetime(string='Opening Date', readonly=True) stop_at = fields.Datetime(string='Closing Date', readonly=True, copy=False) state = fields.Selection(POS_SESSION_STATE, string='Status', required=True, readonly=True, index=True, copy=False, default='opening_control') sequence_number = fields.Integer( string='Order Sequence Number', help='A sequence number that is incremented with each order', default=1) login_number = fields.Integer( string='Login Sequence Number', help= 'A sequence number that is incremented each time a user resumes the pos session', default=0) cash_control = fields.Boolean(compute='_compute_cash_all', string='Has Cash Control') cash_journal_id = fields.Many2one('account.journal', compute='_compute_cash_all', string='Cash Journal', store=True) cash_register_id = fields.Many2one('account.bank.statement', compute='_compute_cash_all', string='Cash Register', store=True) cash_register_balance_end_real = fields.Monetary( related='cash_register_id.balance_end_real', string="Ending Balance", help="Total of closing cash control lines.", readonly=True) cash_register_balance_start = fields.Monetary( related='cash_register_id.balance_start', string="Starting Balance", help="Total of opening cash control lines.", readonly=True) cash_register_total_entry_encoding = fields.Monetary( related='cash_register_id.total_entry_encoding', string='Total Cash Transaction', readonly=True, help="Total of all paid sales orders") cash_register_balance_end = fields.Monetary( related='cash_register_id.balance_end', digits=0, string="Theoretical Closing Balance", help="Sum of opening balance and transactions.", readonly=True) cash_register_difference = fields.Monetary( related='cash_register_id.difference', string='Difference', help= "Difference between the theoretical closing balance and the real closing balance.", readonly=True) journal_ids = fields.Many2many('account.journal', related='config_id.journal_ids', readonly=True, string='Available Payment Methods') order_ids = fields.One2many('pos.order', 'session_id', string='Orders') statement_ids = fields.One2many('account.bank.statement', 'pos_session_id', string='Bank Statement', readonly=True) picking_count = fields.Integer(compute='_compute_picking_count') rescue = fields.Boolean( string='Recovery Session', help="Auto-generated session for orphan orders, ignored in constraints", readonly=True, copy=False) _sql_constraints = [('uniq_name', 'unique(name)', "The name of this POS Session must be unique !")] @api.multi def _compute_picking_count(self): for pos in self: pickings = pos.order_ids.mapped('picking_id').filtered( lambda x: x.state != 'done') pos.picking_count = len(pickings.ids) @api.multi def action_stock_picking(self): pickings = self.order_ids.mapped('picking_id').filtered( lambda x: x.state != 'done') action_picking = self.env.ref('stock.action_picking_tree_ready') action = action_picking.read()[0] action['context'] = {} action['domain'] = [('id', 'in', pickings.ids)] return action @api.depends('config_id', 'statement_ids') def _compute_cash_all(self): for session in self: session.cash_journal_id = session.cash_register_id = session.cash_control = False if session.config_id.cash_control: for statement in session.statement_ids: if statement.journal_id.type == 'cash': session.cash_control = True session.cash_journal_id = statement.journal_id.id session.cash_register_id = statement.id if not session.cash_control and session.state != 'closed': raise UserError( _("Cash control can only be applied to cash journals.") ) @api.constrains('user_id', 'state') def _check_unicity(self): # open if there is no session in 'opening_control', 'opened', 'closing_control' for one user if self.search_count([('state', 'not in', ('closed', 'closing_control')), ('user_id', '=', self.user_id.id), ('rescue', '=', False)]) > 1: raise ValidationError( _("You cannot create two active sessions with the same responsible." )) @api.constrains('config_id') def _check_pos_config(self): if self.search_count([('state', '!=', 'closed'), ('config_id', '=', self.config_id.id), ('rescue', '=', False)]) > 1: raise ValidationError( _("Another session is already opened for this point of sale.")) @api.model def create(self, values): config_id = values.get('config_id') or self.env.context.get( 'default_config_id') if not config_id: raise UserError( _("You should assign a Point of Sale to your session.")) # journal_id is not required on the pos_config because it does not # exists at the installation. If nothing is configured at the # installation we do the minimal configuration. Impossible to do in # the .xml files as the CoA is not yet installed. pos_config = self.env['pos.config'].browse(config_id) ctx = dict(self.env.context, company_id=pos_config.company_id.id) if not pos_config.journal_id: default_journals = pos_config.with_context(ctx).default_get( ['journal_id', 'invoice_journal_id']) if (not default_journals.get('journal_id') or not default_journals.get('invoice_journal_id')): raise UserError( _("Unable to open the session. You have to assign a sales journal to your point of sale." )) pos_config.with_context(ctx).sudo().write({ 'journal_id': default_journals['journal_id'], 'invoice_journal_id': default_journals['invoice_journal_id'] }) # define some cash journal if no payment method exists if not pos_config.journal_ids: Journal = self.env['account.journal'] journals = Journal.with_context(ctx).search([ ('journal_user', '=', True), ('type', '=', 'cash') ]) if not journals: journals = Journal.with_context(ctx).search([('type', '=', 'cash')]) if not journals: journals = Journal.with_context(ctx).search([ ('journal_user', '=', True) ]) if not journals: raise ValidationError( _("No payment method configured! \nEither no Chart of Account is installed or no payment method is configured for this POS." )) journals.sudo().write({'journal_user': True}) pos_config.sudo().write({'journal_ids': [(6, 0, journals.ids)]}) pos_name = self.env['ir.sequence'].with_context(ctx).next_by_code( 'pos.session') if values.get('name'): pos_name += ' ' + values['name'] statements = [] ABS = self.env['account.bank.statement'] uid = SUPERUSER_ID if self.env.user.has_group( 'point_of_sale.group_pos_user') else self.env.user.id for journal in pos_config.journal_ids: # set the journal_id which should be used by # account.bank.statement to set the opening balance of the # newly created bank statement ctx['journal_id'] = journal.id if pos_config.cash_control and journal.type == 'cash' else False st_values = { 'journal_id': journal.id, 'user_id': self.env.user.id, 'name': pos_name, 'balance_start': self.env["account.bank.statement"]._get_opening_balance( journal.id) if journal.type == 'cash' else 0 } statements.append( ABS.with_context(ctx).sudo(uid).create(st_values).id) values.update({ 'name': pos_name, 'statement_ids': [(6, 0, statements)], 'config_id': config_id }) res = super(PosSession, self.with_context(ctx).sudo(uid)).create(values) if not pos_config.cash_control: res.action_pos_session_open() return res @api.multi def unlink(self): for session in self.filtered(lambda s: s.statement_ids): session.statement_ids.unlink() return super(PosSession, self).unlink() @api.multi def login(self): self.ensure_one() self.write({ 'login_number': self.login_number + 1, }) @api.multi def action_pos_session_open(self): # second browse because we need to refetch the data from the DB for cash_register_id # we only open sessions that haven't already been opened for session in self.filtered( lambda session: session.state == 'opening_control'): values = {} if not session.start_at: values['start_at'] = fields.Datetime.now() values['state'] = 'opened' session.write(values) session.statement_ids.button_open() return True @api.multi def action_pos_session_closing_control(self): self._check_pos_session_balance() for session in self: session.write({ 'state': 'closing_control', 'stop_at': fields.Datetime.now() }) if not session.config_id.cash_control: session.action_pos_session_close() @api.multi def _check_pos_session_balance(self): for session in self: for statement in session.statement_ids: if (statement != session.cash_register_id) and ( statement.balance_end != statement.balance_end_real): statement.write( {'balance_end_real': statement.balance_end}) @api.multi def action_pos_session_validate(self): self._check_pos_session_balance() self.action_pos_session_close() @api.multi def action_pos_session_close(self): # Close CashBox for session in self: company_id = session.config_id.company_id.id ctx = dict(self.env.context, force_company=company_id, company_id=company_id) ctx_notrack = dict(ctx, mail_notrack=True) for st in session.statement_ids: if abs(st.difference) > st.journal_id.amount_authorized_diff: # The pos manager can close statements with maximums. if not self.user_has_groups( "point_of_sale.group_pos_manager"): raise UserError( _("Your ending balance is too different from the theoretical cash closing (%.2f), the maximum allowed is: %.2f. You can contact your manager to force it." ) % (st.difference, st.journal_id.amount_authorized_diff)) if (st.journal_id.type not in ['bank', 'cash']): raise UserError( _("The journal type for your payment method should be bank or cash." )) st.with_context(ctx_notrack).sudo().button_confirm_bank() self.with_context(ctx)._confirm_orders() self.write({'state': 'closed'}) return { 'type': 'ir.actions.client', 'name': 'Point of Sale Menu', 'tag': 'reload', 'params': { 'menu_id': self.env.ref('point_of_sale.menu_point_root').id }, } @api.multi def open_frontend_cb(self): if not self.ids: return {} for session in self.filtered(lambda s: s.user_id.id != self.env.uid): raise UserError( _("You cannot use the session of another user. This session is owned by %s. " "Please first close this one to use this point of sale.") % session.user_id.name) return { 'type': 'ir.actions.act_url', 'target': 'self', 'url': '/pos/web/', } @api.multi def open_cashbox(self): self.ensure_one() context = dict(self._context) balance_type = context.get('balance') or 'start' context['bank_statement_id'] = self.cash_register_id.id context['balance'] = balance_type context['default_pos_id'] = self.config_id.id action = { 'name': _('Cash Control'), 'view_type': 'form', 'view_mode': 'form', 'res_model': 'account.bank.statement.cashbox', 'view_id': self.env.ref('account.view_account_bnk_stmt_cashbox').id, 'type': 'ir.actions.act_window', 'context': context, 'target': 'new' } cashbox_id = None if balance_type == 'start': cashbox_id = self.cash_register_id.cashbox_start_id.id else: cashbox_id = self.cash_register_id.cashbox_end_id.id if cashbox_id: action['res_id'] = cashbox_id return action
class ResConfigSettings(models.TransientModel): _inherit = 'res.config.settings' app_system_name = fields.Char( 'System Name', help=u"Setup System Name,which replace Eagle") app_show_lang = fields.Boolean( 'Show Quick Language Switcher', help=u"When enable,User can quick switch language in user menu") app_show_debug = fields.Boolean( 'Show Quick Debug', help=u"When enable,everyone login can see the debug menu") app_show_documentation = fields.Boolean( 'Show Documentation', help=u"When enable,User can visit user manual") app_show_documentation_dev = fields.Boolean( 'Show Developer Documentation', help=u"When enable,User can visit development documentation") app_show_support = fields.Boolean( 'Show Support', help=u"When enable,User can vist your support site") app_show_account = fields.Boolean( 'Show My Account', help=u"When enable,User can login to your website") app_show_enterprise = fields.Boolean( 'Show Enterprise Tag', help=u"Uncheck to hide the Enterprise tag") app_show_share = fields.Boolean( 'Show Share Dashboard', help=u"Uncheck to hide the Eagle Share Dashboard") app_show_poweredby = fields.Boolean( 'Show Powered by Eagle', help=u"Uncheck to hide the Powered by text") app_stop_subscribe = fields.Boolean( 'Stop Eagle Subscribe(Performance Improve)', help=u"Check to stop Eagle Subscribe function") group_show_author_in_apps = fields.Boolean( string="Show Author in Apps Dashboard", implied_group='app_eagle_customize.group_show_author_in_apps', help=u"Uncheck to Hide Author and Website in Apps Dashboard") app_documentation_url = fields.Char('Documentation Url') app_documentation_dev_url = fields.Char('Developer Documentation Url') app_support_url = fields.Char('Support Url') app_account_title = fields.Char('My Eagle.com Account Title') app_account_url = fields.Char('My Eagle.com Account Url') app_enterprise_url = fields.Char('Customize Module Url(eg. Enterprise)') @api.model def get_values(self): res = super(ResConfigSettings, self).get_values() ir_config = self.env['ir.config_parameter'].sudo() app_system_name = ir_config.get_param('app_system_name', default='eagleApp') app_show_lang = True if ir_config.get_param( 'app_show_lang') == "True" else False app_show_debug = True if ir_config.get_param( 'app_show_debug') == "True" else False app_show_documentation = True if ir_config.get_param( 'app_show_documentation') == "True" else False app_show_documentation_dev = True if ir_config.get_param( 'app_show_documentation_dev') == "True" else False app_show_support = True if ir_config.get_param( 'app_show_support') == "True" else False app_show_account = True if ir_config.get_param( 'app_show_account') == "True" else False app_show_enterprise = True if ir_config.get_param( 'app_show_enterprise') == "True" else False app_show_share = True if ir_config.get_param( 'app_show_share') == "True" else False app_show_poweredby = True if ir_config.get_param( 'app_show_poweredby') == "True" else False app_stop_subscribe = True if ir_config.get_param( 'app_stop_subscribe') == "True" else False app_documentation_url = ir_config.get_param( 'app_documentation_url', default= 'https://www.sunpop.cn/documentation/user/12.0/en/index.html') app_documentation_dev_url = ir_config.get_param( 'app_documentation_dev_url', default='https://www.sunpop.cn/documentation/12.0/index.html') app_support_url = ir_config.get_param( 'app_support_url', default='https://www.sunpop.cn/trial/') app_account_title = ir_config.get_param('app_account_title', default='My Online Account') app_account_url = ir_config.get_param( 'app_account_url', default='https://www.sunpop.cn/my-account/') app_enterprise_url = ir_config.get_param( 'app_enterprise_url', default='https://www.sunpop.cn') res.update(app_system_name=app_system_name, app_show_lang=app_show_lang, app_show_debug=app_show_debug, app_show_documentation=app_show_documentation, app_show_documentation_dev=app_show_documentation_dev, app_show_support=app_show_support, app_show_account=app_show_account, app_show_enterprise=app_show_enterprise, app_show_share=app_show_share, app_show_poweredby=app_show_poweredby, app_stop_subscribe=app_stop_subscribe, app_documentation_url=app_documentation_url, app_documentation_dev_url=app_documentation_dev_url, app_support_url=app_support_url, app_account_title=app_account_title, app_account_url=app_account_url, app_enterprise_url=app_enterprise_url) return res @api.multi def set_values(self): super(ResConfigSettings, self).set_values() ir_config = self.env['ir.config_parameter'].sudo() ir_config.set_param("app_system_name", self.app_system_name or "") ir_config.set_param("app_show_lang", self.app_show_lang or "False") ir_config.set_param("app_show_debug", self.app_show_debug or "False") ir_config.set_param("app_show_documentation", self.app_show_documentation or "False") ir_config.set_param("app_show_documentation_dev", self.app_show_documentation_dev or "False") ir_config.set_param("app_show_support", self.app_show_support or "False") ir_config.set_param("app_show_account", self.app_show_account or "False") ir_config.set_param("app_show_enterprise", self.app_show_enterprise or "False") ir_config.set_param("app_show_share", self.app_show_share or "False") ir_config.set_param("app_show_poweredby", self.app_show_poweredby or "False") ir_config.set_param("app_stop_subscribe", self.app_stop_subscribe or "False") ir_config.set_param( "app_documentation_url", self.app_documentation_url or "https://www.sunpop.cn/documentation/user/12.0/en/index.html") ir_config.set_param( "app_documentation_dev_url", self.app_documentation_dev_url or "https://www.sunpop.cn/documentation/12.0/index.html") ir_config.set_param( "app_support_url", self.app_support_url or "https://www.sunpop.cn/trial/") ir_config.set_param("app_account_title", self.app_account_title or "My Online Account") ir_config.set_param( "app_account_url", self.app_account_url or "https://www.sunpop.cn/my-account/") ir_config.set_param("app_enterprise_url", self.app_enterprise_url or "https://www.sunpop.cn") def set_module_url(self): sql = "UPDATE ir_module_module SET website = '%s' WHERE license like '%s' and website <> ''" % ( self.app_enterprise_url, 'OEEL%') try: self._cr.execute(sql) except Exception as e: pass def remove_sales(self): to_removes = [ # 清除销售单据 [ 'sale.order.line', ], [ 'sale.order', ], # 销售提成,自用 [ 'sale.commission.line', ], # 不能删除报价单模板 # ['sale.order.template.option', ], # ['sale.order.template.line', ], # ['sale.order.template', ], ] try: for line in to_removes: obj_name = line[0] obj = self.pool.get(obj_name) if obj: sql = "delete from %s" % obj._table self._cr.execute(sql) # 更新序号 seqs = self.env['ir.sequence'].search([ '|', ('code', '=', 'sale.order'), ('code', '=', 'sale.commission.line') ]) for seq in seqs: seq.write({ 'number_next': 1, }) except Exception as e: raise Warning(e) return True def remove_product(self): to_removes = [ # 清除产品数据 [ 'product.product', ], [ 'product.template', ], ] try: for line in to_removes: obj_name = line[0] obj = self.pool.get(obj_name) if obj: sql = "delete from %s" % obj._table self._cr.execute(sql) # 更新序号,针对自动产品编号 seqs = self.env['ir.sequence'].search([('code', '=', 'product.product')]) for seq in seqs: seq.write({ 'number_next': 1, }) except Exception as e: pass # raise Warning(e) return True def remove_product_attribute(self): to_removes = [ # 清除产品属性 [ 'product.attribute.value', ], [ 'product.attribute', ], ] try: for line in to_removes: obj_name = line[0] obj = self.pool.get(obj_name) if obj: sql = "delete from %s" % obj._table self._cr.execute(sql) except Exception as e: pass # raise Warning(e) return True @api.multi def remove_pos(self): to_removes = [ # 清除POS单据 [ 'pos.order.line', ], [ 'pos.order', ], ] try: for line in to_removes: obj_name = line[0] obj = self.pool.get(obj_name) if obj: sql = "delete from %s" % obj._table self._cr.execute(sql) # 更新序号 seqs = self.env['ir.sequence'].search([('code', '=', 'pos.order')]) for seq in seqs: seq.write({ 'number_next': 1, }) # 更新要关帐的值,因为 store=true 的计算字段要重置 statement = self.env['account.bank.statement'].search([]) for s in statement: s._end_balance() except Exception as e: pass # raise Warning(e) return True @api.multi def remove_purchase(self): to_removes = [ # 清除采购单据 [ 'purchase.order.line', ], [ 'purchase.order', ], [ 'purchase.requisition.line', ], [ 'purchase.requisition', ], ] try: for line in to_removes: obj_name = line[0] obj = self.pool.get(obj_name) if obj: sql = "delete from %s" % obj._table self._cr.execute(sql) # 更新序号 seqs = self.env['ir.sequence'].search([ '|', ('code', '=', 'purchase.order'), '|', ('code', '=', 'purchase.requisition.purchase.tender'), ('code', '=', 'purchase.requisition.blanket.order') ]) for seq in seqs: seq.write({ 'number_next': 1, }) self._cr.execute(sql) except Exception as e: pass # raise Warning(e) return True @api.multi def remove_expense(self): to_removes = [ # 清除采购单据 [ 'hr.expense.sheet', ], [ 'hr.expense', ], ] try: for line in to_removes: obj_name = line[0] obj = self.pool.get(obj_name) if obj: sql = "delete from %s" % obj._table self._cr.execute(sql) # 更新序号 seqs = self.env['ir.sequence'].search([('code', '=', 'hr.expense.invoice')]) for seq in seqs: seq.write({ 'number_next': 1, }) self._cr.execute(sql) except Exception as e: pass # raise Warning(e) return True @api.multi def remove_expense(self): to_removes = [ # 清除 [ 'hr.expense.sheet', ], [ 'hr.expense', ], [ 'hr.payslip', ], [ 'hr.payslip.run', ], ] try: for line in to_removes: obj_name = line[0] obj = self.pool.get(obj_name) if obj: sql = "delete from %s" % obj._table self._cr.execute(sql) # 更新序号 seqs = self.env['ir.sequence'].search([('code', '=', 'hr.expense.invoice')]) for seq in seqs: seq.write({ 'number_next': 1, }) self._cr.execute(sql) except Exception as e: pass # raise Warning(e) return True @api.multi def remove_mrp(self): to_removes = [ # 清除生产单据 [ 'mrp.workcenter.productivity', ], [ 'mrp.workorder', ], [ 'mrp.production.workcenter.line', ], [ 'change.production.qty', ], [ 'mrp.production', ], [ 'mrp.production.product.line', ], [ 'mrp.unbuild', ], [ 'change.production.qty', ], [ 'sale.forecast.indirect', ], [ 'sale.forecast', ], ] try: for line in to_removes: obj_name = line[0] obj = self.pool.get(obj_name) if obj: sql = "delete from %s" % obj._table self._cr.execute(sql) # 更新序号 seqs = self.env['ir.sequence'].search([ '|', ('code', '=', 'mrp.production'), ('code', '=', 'mrp.unbuild'), ]) for seq in seqs: seq.write({ 'number_next': 1, }) except Exception as e: pass # raise Warning(e) return True @api.multi def remove_mrp_bom(self): to_removes = [ # 清除生产BOM [ 'mrp.bom.line', ], [ 'mrp.bom', ], ] try: for line in to_removes: obj_name = line[0] obj = self.pool.get(obj_name) if obj: sql = "delete from %s" % obj._table self._cr.execute(sql) except Exception as e: pass # raise Warning(e) return True @api.multi def remove_inventory(self): to_removes = [ # 清除库存单据 [ 'stock.quant', ], [ 'stock.move.line', ], [ 'stock.package.level', ], [ 'stock.quantity.history', ], [ 'stock.quant.package', ], [ 'stock.move', ], [ 'stock.pack.operation', ], [ 'stock.picking', ], [ 'stock.scrap', ], [ 'stock.picking.batch', ], [ 'stock.inventory.line', ], [ 'stock.inventory', ], [ 'stock.production.lot', ], [ 'stock.fixed.putaway.strat', ], [ 'procurement.group', ], ] try: for line in to_removes: obj_name = line[0] obj = self.pool.get(obj_name) if obj: sql = "delete from %s" % obj._table self._cr.execute(sql) # 更新序号 seqs = self.env['ir.sequence'].search([ '|', ('code', '=', 'stock.lot.serial'), '|', ('code', '=', 'stock.lot.tracking'), '|', ('code', '=', 'stock.orderpoint'), '|', ('code', '=', 'stock.picking'), '|', ('code', '=', 'picking.batch'), '|', ('code', '=', 'stock.quant.package'), '|', ('code', '=', 'stock.scrap'), '|', ('code', '=', 'stock.picking'), '|', ('prefix', '=', 'WH/IN/'), '|', ('prefix', '=', 'WH/INT/'), '|', ('prefix', '=', 'WH/OUT/'), '|', ('prefix', '=', 'WH/PACK/'), ('prefix', '=', 'WH/PICK/') ]) for seq in seqs: seq.write({ 'number_next': 1, }) except Exception as e: pass # raise Warning(e) return True @api.multi def remove_account(self): to_removes = [ # 清除财务会计单据 [ 'account.voucher.line', ], [ 'account.voucher', ], [ 'account.bank.statement.line', ], [ 'account.payment', ], [ 'account.analytic.line', ], [ 'account.analytic.account', ], [ 'account.invoice.line', ], [ 'account.invoice.refund', ], [ 'account.invoice', ], [ 'account.partial.reconcile', ], [ 'account.move.line', ], [ 'hr.expense.sheet', ], [ 'account.move', ], ] try: for line in to_removes: obj_name = line[0] obj = self.pool.get(obj_name) if obj: sql = "delete from %s" % obj._table self._cr.execute(sql) # 更新序号 seqs = self.env['ir.sequence'].search([ '|', ('code', '=', 'account.reconcile'), '|', ('code', '=', 'account.payment.customer.invoice'), '|', ('code', '=', 'account.payment.customer.refund'), '|', ('code', '=', 'account.payment.supplier.invoice'), '|', ('code', '=', 'account.payment.supplier.refund'), '|', ('code', '=', 'account.payment.transfer'), '|', ('prefix', 'like', 'BNK1/'), '|', ('prefix', 'like', 'CSH1/'), '|', ('prefix', 'like', 'INV/'), '|', ('prefix', 'like', 'EXCH/'), '|', ('prefix', 'like', 'MISC/'), '|', ('prefix', 'like', '账单/'), ('prefix', 'like', '杂项/') ]) for seq in seqs: seq.write({ 'number_next': 1, }) except Exception as e: pass # raise Warning(e) return True @api.multi def remove_account_chart(self): to_removes = [ # 清除财务科目,用于重设 [ 'res.partner.bank', ], [ 'res.bank', ], ['account.move.line'], ['account.invoice'], ['account.payment'], [ 'account.bank.statement', ], [ 'account.tax.account.tag', ], [ 'account.tax', ], [ 'account.tax', ], [ 'account.account.account.tag', ], ['wizard_multi_charts_accounts'], [ 'account.account', ], [ 'account.journal', ], ] # todo: 要做 remove_hr,因为工资表会用到 account # 更新account关联,很多是多公司字段,故只存在 ir_property,故在原模型,只能用update try: # reset default tax,不管多公司 field1 = self.env['ir.model.fields']._get('product.template', "taxes_id").id field2 = self.env['ir.model.fields']._get('product.template', "supplier_taxes_id").id sql = ( "delete from ir_default where field_id = %s or field_id = %s" ) % (field1, field2) self._cr.execute(sql) except Exception as e: pass # raise Warning(e) try: rec = self.env['res.partner'].search([]) for r in rec: r.write({ 'property_account_receivable_id': None, 'property_account_payable_id': None, }) except Exception as e: pass # raise Warning(e) try: rec = self.env['product.category'].search([]) for r in rec: r.write({ 'property_account_income_categ_id': None, 'property_account_expense_categ_id': None, 'property_account_creditor_price_difference_categ': None, 'property_stock_account_input_categ_id': None, 'property_stock_account_output_categ_id': None, 'property_stock_valuation_account_id': None, }) except Exception as e: pass # raise Warning(e) try: rec = self.env['stock.location'].search([]) for r in rec: r.write({ 'valuation_in_account_id': None, 'valuation_out_account_id': None, }) except Exception as e: pass # raise Warning(e) try: for line in to_removes: obj_name = line[0] obj = self.pool.get(obj_name) if obj: sql = "delete from %s" % obj._table self._cr.execute(sql) sql = "update res_company set chart_template_id=null;" self._cr.execute(sql) # 更新序号 except Exception as e: pass return True @api.multi def remove_project(self): to_removes = [ # 清除项目 [ 'account.analytic.line', ], [ 'project.task', ], [ 'project.forecast', ], [ 'project.project', ], ] try: for line in to_removes: obj_name = line[0] obj = self.pool.get(obj_name) if obj: sql = "delete from %s" % obj._table self._cr.execute(sql) # 更新序号 except Exception as e: pass # raise Warning(e) return True @api.multi def remove_website(self): to_removes = [ # 清除网站数据,w, w_blog [ 'blog.tag.category', ], [ 'blog.tag', ], [ 'blog.post', ], [ 'blog.blog', ], [ 'website.published.multi.mixin', ], [ 'website.published.mixin', ], [ 'website.multi.mixin', ], [ 'website.redirect', ], [ 'website.seo.metadata', ], [ 'website.page', ], [ 'website.menu', ], [ 'website', ], ] try: for line in to_removes: obj_name = line[0] obj = self.pool.get(obj_name) if obj and obj._table: sql = "delete from %s" % obj._table self._cr.execute(sql) except Exception as e: pass # raise Warning(e) return True @api.multi def remove_message(self): to_removes = [ # 清除消息数据 [ 'mail.message', ], [ 'mail.followers', ], ] try: for line in to_removes: obj_name = line[0] obj = self.pool.get(obj_name) if obj and obj._table: sql = "delete from %s" % obj._table self._cr.execute(sql) except Exception as e: pass # raise Warning(e) return True @api.multi def remove_workflow(self): to_removes = [ # 清除工作流 [ 'wkf.workitem', ], [ 'wkf.instance', ], ] try: for line in to_removes: obj_name = line[0] obj = self.pool.get(obj_name) if obj and obj._table: sql = "delete from %s" % obj._table self._cr.execute(sql) except Exception as e: pass # raise Warning(e) return True @api.multi def remove_all_biz(self): try: self.remove_account() self.remove_inventory() self.remove_mrp() self.remove_purchase() self.remove_sales() self.remove_project() self.remove_pos() self.remove_expense() self.remove_message() except Exception as e: pass # raise Warning(e) return True
class FeatureProductSlider(models.Model): _name = 'feature.product.slider.config' _description = 'Featured Products Slider' name = fields.Char(string="Name", default='My Products Slider', required=True, translate=True, help="""Slider name will not be visible in website it is only for unique identification while dragging the snippet in website.""" ) active = fields.Boolean(string="Active", default=True) feature_name = fields.Char(string="Featured Products Slider Label", default='Featured Products', required=True, translate=True, help="""Slider title to be displayed on website like Featured Products, Latest and etc...""") feature_products_collections = fields.Many2many( 'product.template', 'theme_eagleshop12_feature_pro_colle_slider_rel', 'slider_id', 'prod_id', required=True, string="Featured Products Collections") on_sale_name = fields.Char(string="On Sale Slider Label", default='On Sale', required=True, translate=True, help="""Slider title to be displayed on website like On Sale, Latest and etc...""") on_sale_collections = fields.Many2many( 'product.template', 'theme_eagleshop12_on_sale_name_collections_slider_rel', 'slider_id', 'prod_id', required=True, string="Sale Products Collections") random_name = fields.Char(string="Random Products Slider Label", default='Random Products', required=True, translate=True, help="""Slider title to be displayed on website like Random Products, Latest and etc...""") random_products_collections = fields.Many2many( 'product.template', 'theme_eagleshop12_random_product_coll_slider_rel', 'slider_id', 'prod_id', required=True, string="Random Products Collections") low_price_name = fields.Char( string="Low Price Slider Label", default='Low Price', required=True, translate=True, help="""Slider title to be displayed on website like Low Price, Latest and etc...""") low_price_collections = fields.Many2many( 'product.template', 'theme_eagleshop12_low_price_products_collec_slider_rel', 'slider_id', 'prod_id', required=True, string="Low Products Collections")
class SMSResend(models.TransientModel): _name = 'sms.resend' _description = 'SMS Resend' _rec_name = 'mail_message_id' @api.model def default_get(self, fields): result = super(SMSResend, self).default_get(fields) if result.get('mail_message_id'): mail_message_id = self.env['mail.message'].browse( result['mail_message_id']) result['recipient_ids'] = [ (0, 0, { 'notification_id': notif.id, 'resend': True, 'failure_type': notif.failure_type, 'partner_name': notif.res_partner_id.display_name or mail_message_id.record_name, 'sms_number': notif.sms_number, }) for notif in mail_message_id.notification_ids if notif.notification_type == 'sms' and notif.notification_status in ('exception', 'bounce') ] return result mail_message_id = fields.Many2one('mail.message', 'Message', readonly=True, required=True) recipient_ids = fields.One2many('sms.resend.recipient', 'sms_resend_id', string='Recipients') has_cancel = fields.Boolean(compute='_compute_has_cancel') has_insufficient_credit = fields.Boolean( compute='_compute_has_insufficient_credit') @api.depends("recipient_ids.failure_type") def _compute_has_insufficient_credit(self): self.has_insufficient_credit = self.recipient_ids.filtered( lambda p: p.failure_type == 'sms_credit') @api.depends("recipient_ids.resend") def _compute_has_cancel(self): self.has_cancel = self.recipient_ids.filtered(lambda p: not p.resend) def _check_access(self): if not self.mail_message_id or not self.mail_message_id.model or not self.mail_message_id.res_id: raise exceptions.UserError( _('You do not have access to the message and/or related document.' )) record = self.env[self.mail_message_id.model].browse( self.mail_message_id.res_id) record.check_access_rights('read') record.check_access_rule('read') def action_resend(self): self._check_access() all_notifications = self.env['mail.notification'].sudo().search([ ('mail_message_id', '=', self.mail_message_id.id), ('notification_type', '=', 'sms'), ('notification_status', 'in', ('exception', 'bounce')) ]) sudo_self = self.sudo() to_cancel_ids = [ r.notification_id.id for r in sudo_self.recipient_ids if not r.resend ] to_resend_ids = [ r.notification_id.id for r in sudo_self.recipient_ids if r.resend ] if to_cancel_ids: all_notifications.filtered(lambda n: n.id in to_cancel_ids).write( {'notification_status': 'canceled'}) if to_resend_ids: record = self.env[self.mail_message_id.model].browse( self.mail_message_id.res_id) sms_pid_to_number = dict((r.partner_id.id, r.sms_number) for r in self.recipient_ids if r.resend and r.partner_id) pids = list(sms_pid_to_number.keys()) numbers = [ r.sms_number for r in self.recipient_ids if r.resend and not r.partner_id ] rdata = [] for pid, cid, active, pshare, ctype, notif, groups in self.env[ 'mail.followers']._get_recipient_data(record, 'sms', False, pids=pids): if pid and notif == 'sms': rdata.append({ 'id': pid, 'share': pshare, 'active': active, 'notif': notif, 'groups': groups or [], 'type': 'customer' if pshare else 'user' }) if rdata or numbers: record._notify_record_by_sms( self.mail_message_id, {'partners': rdata}, check_existing=True, sms_numbers=numbers, sms_pid_to_number=sms_pid_to_number, put_in_queue=False) self.mail_message_id._notify_sms_update() return {'type': 'ir.actions.act_window_close'} def action_cancel(self): self._check_access() sudo_self = self.sudo() sudo_self.mapped('recipient_ids.notification_id').write( {'notification_status': 'canceled'}) self.mail_message_id._notify_sms_update() return {'type': 'ir.actions.act_window_close'} def action_buy_credits(self): url = self.env['iap.account'].get_credits_url(service_name='sms') return { 'type': 'ir.actions.act_url', 'url': url, }
class MultiSlider(models.Model): _name = 'multi.slider.config' _description = 'Product Multi Slider' name = fields.Char(string="Slider name", default='Trending', required=True, translate=True, help="""Slider title to be displayed on website like Best products, Latest and etc...""") active = fields.Boolean(string="Active", default=True) auto_rotate = fields.Boolean(string='Auto Rotate Slider', default=True) sliding_speed = fields.Integer(string="Slider sliding speed", default='5000', help='''Sliding speed of a slider can be set from here and it will be in milliseconds.''' ) no_of_collection = fields.Selection( [('2', '2'), ('3', '3'), ('4', '4'), ('5', '5')], string="No. of collections to show", default='2', required=True, help="No of collections to be displayed on slider.") label_collection_1 = fields.Char( string="1st collection name", default='First collection', required=True, help="""Collection label to be displayed in website like Featured, Trending, Best Sellers, etc...""", translate=True) collection_1_ids = fields.Many2many('product.template', 'product_slider_collection_1_rel', 'slider_id', 'prod_id', required=True, string="1st product collection") special_offer_1_product_tmpl_id = fields.Many2one( 'product.template', required=True, string='''Special Offer product for 1st collection''') label_collection_2 = fields.Char(string="2nd collection name", default='Second collection', required=True, translate=True, help="""Collection label to be displayed in website like Featured, Trending, Best Sellers, etc...""" ) collection_2_ids = fields.Many2many('product.template', 'product_slider_collection_2_rel', 'slider_id', 'prod_id', required=True, string="2nd product collection") special_offer_2_product_tmpl_id = fields.Many2one('product.template', required=True, string='''Special Offer product for 2nd collection''' ) label_collection_3 = fields.Char(string="3rd collection name", default='Third collection', translate=True, help="""Collection label to be displayed in website like Featured, Trending, Best Sellers, etc...""" ) collection_3_ids = fields.Many2many('product.template', 'product_slider_collection_3_rel', 'slider_id', 'prod_id', string="3rd product collection") special_offer_3_product_tmpl_id = fields.Many2one( 'product.template', string='Special Offer product for 3rd collection') label_collection_4 = fields.Char( string="4th collection name", default='Fourth collection', translate=True, help="""Collection label to be displayed in website like Featured, Trending, Best Sellers, etc...""" ) collection_4_ids = fields.Many2many('product.template', 'product_slider_collection_4_rel', 'slider_id', 'prod_id', string="4th product collection") special_offer_4_product_tmpl_id = fields.Many2one( 'product.template', string='Special Offer product for 4th collection') label_collection_5 = fields.Char(string="5th collection name", default='Fifth collection', translate=True, help="""Collection label to be displayed in website like Featured, Trending, Best Sellers, etc...""") collection_5_ids = fields.Many2many('product.template', 'product_slider_collection_5_rel', 'slider_id', 'prod_id', string="5th product collection") special_offer_5_product_tmpl_id = fields.Many2one( 'product.template', string='Special Offer product for 5th collection')
class Challenge(models.Model): """Gamification challenge Set of predifined objectives assigned to people with rules for recurrence and rewards If 'user_ids' is defined and 'period' is different than 'one', the set will be assigned to the users for each period (eg: every 1st of each month if 'monthly' is selected) """ _name = 'gamification.challenge' _description = 'Gamification Challenge' _inherit = 'mail.thread' _order = 'end_date, start_date, name, id' name = fields.Char("Challenge Name", required=True, translate=True) description = fields.Text("Description", translate=True) state = fields.Selection([ ('draft', "Draft"), ('inprogress', "In Progress"), ('done', "Done"), ], default='draft', copy=False, string="State", required=True, tracking=True) manager_id = fields.Many2one( 'res.users', default=lambda self: self.env.uid, string="Responsible", help="The user responsible for the challenge.",) user_ids = fields.Many2many('res.users', 'gamification_challenge_users_rel', string="Users", help="List of users participating to the challenge") user_domain = fields.Char("User domain", help="Alternative to a list of users") period = fields.Selection([ ('once', "Non recurring"), ('daily', "Daily"), ('weekly', "Weekly"), ('monthly', "Monthly"), ('yearly', "Yearly") ], default='once', string="Periodicity", help="Period of automatic goal assigment. If none is selected, should be launched manually.", required=True) start_date = fields.Date("Start Date", help="The day a new challenge will be automatically started. If no periodicity is set, will use this date as the goal start date.") end_date = fields.Date("End Date", help="The day a new challenge will be automatically closed. If no periodicity is set, will use this date as the goal end date.") invited_user_ids = fields.Many2many('res.users', 'gamification_invited_user_ids_rel', string="Suggest to users") line_ids = fields.One2many('gamification.challenge.line', 'challenge_id', string="Lines", help="List of goals that will be set", required=True, copy=True) reward_id = fields.Many2one('gamification.badge', string="For Every Succeeding User") reward_first_id = fields.Many2one('gamification.badge', string="For 1st user") reward_second_id = fields.Many2one('gamification.badge', string="For 2nd user") reward_third_id = fields.Many2one('gamification.badge', string="For 3rd user") reward_failure = fields.Boolean("Reward Bests if not Succeeded?") reward_realtime = fields.Boolean("Reward as soon as every goal is reached", default=True, help="With this option enabled, a user can receive a badge only once. The top 3 badges are still rewarded only at the end of the challenge.") visibility_mode = fields.Selection([ ('personal', "Individual Goals"), ('ranking', "Leader Board (Group Ranking)"), ], default='personal', string="Display Mode", required=True) report_message_frequency = fields.Selection([ ('never', "Never"), ('onchange', "On change"), ('daily', "Daily"), ('weekly', "Weekly"), ('monthly', "Monthly"), ('yearly', "Yearly") ], default='never', string="Report Frequency", required=True) report_message_group_id = fields.Many2one('mail.channel', string="Send a copy to", help="Group that will receive a copy of the report in addition to the user") report_template_id = fields.Many2one('mail.template', default=lambda self: self._get_report_template(), string="Report Template", required=True) remind_update_delay = fields.Integer("Non-updated manual goals will be reminded after", help="Never reminded if no value or zero is specified.") last_report_date = fields.Date("Last Report Date", default=fields.Date.today) next_report_date = fields.Date("Next Report Date", compute='_get_next_report_date', store=True) category = fields.Selection([ ('hr', 'Human Resources / Engagement'), ('other', 'Settings / Gamification Tools'), ], string="Appears in", required=True, default='hr', help="Define the visibility of the challenge through menus") REPORT_OFFSETS = { 'daily': timedelta(days=1), 'weekly': timedelta(days=7), 'monthly': relativedelta(months=1), 'yearly': relativedelta(years=1), } @api.depends('last_report_date', 'report_message_frequency') def _get_next_report_date(self): """ Return the next report date based on the last report date and report period. """ for challenge in self: last = challenge.last_report_date offset = self.REPORT_OFFSETS.get(challenge.report_message_frequency) if offset: challenge.next_report_date = last + offset else: challenge.next_report_date = False def _get_report_template(self): template = self.env.ref('gamification.simple_report_template', raise_if_not_found=False) return template.id if template else False @api.model def create(self, vals): """Overwrite the create method to add the user of groups""" if vals.get('user_domain'): users = self._get_challenger_users(ustr(vals.get('user_domain'))) if not vals.get('user_ids'): vals['user_ids'] = [] vals['user_ids'].extend((4, user.id) for user in users) return super(Challenge, self).create(vals) def write(self, vals): if vals.get('user_domain'): users = self._get_challenger_users(ustr(vals.get('user_domain'))) if not vals.get('user_ids'): vals['user_ids'] = [] vals['user_ids'].extend((4, user.id) for user in users) write_res = super(Challenge, self).write(vals) if vals.get('report_message_frequency', 'never') != 'never': # _recompute_challenge_users do not set users for challenges with no reports, subscribing them now for challenge in self: challenge.message_subscribe([user.partner_id.id for user in challenge.user_ids]) if vals.get('state') == 'inprogress': self._recompute_challenge_users() self._generate_goals_from_challenge() elif vals.get('state') == 'done': self._check_challenge_reward(force=True) elif vals.get('state') == 'draft': # resetting progress if self.env['gamification.goal'].search([('challenge_id', 'in', self.ids), ('state', '=', 'inprogress')], limit=1): raise exceptions.UserError(_("You can not reset a challenge with unfinished goals.")) return write_res ##### Update ##### @api.model # FIXME: check how cron functions are called to see if decorator necessary def _cron_update(self, ids=False, commit=True): """Daily cron check. - Start planned challenges (in draft and with start_date = today) - Create the missing goals (eg: modified the challenge to add lines) - Update every running challenge """ # start scheduled challenges planned_challenges = self.search([ ('state', '=', 'draft'), ('start_date', '<=', fields.Date.today()) ]) if planned_challenges: planned_challenges.write({'state': 'inprogress'}) # close scheduled challenges scheduled_challenges = self.search([ ('state', '=', 'inprogress'), ('end_date', '<', fields.Date.today()) ]) if scheduled_challenges: scheduled_challenges.write({'state': 'done'}) records = self.browse(ids) if ids else self.search([('state', '=', 'inprogress')]) # in cron mode, will do intermediate commits # FIXME: replace by parameter return records.with_context(commit_gamification=commit)._update_all() def _update_all(self): """Update the challenges and related goals :param list(int) ids: the ids of the challenges to update, if False will update only challenges in progress.""" if not self: return True Goals = self.env['gamification.goal'] # include yesterday goals to update the goals that just ended # exclude goals for users that did not connect since the last update yesterday = fields.Date.to_string(date.today() - timedelta(days=1)) self.env.cr.execute("""SELECT gg.id FROM gamification_goal as gg, gamification_challenge as gc, res_users as ru, res_users_log as log WHERE gg.challenge_id = gc.id AND gg.user_id = ru.id AND ru.id = log.create_uid AND gg.write_date < log.create_date AND gg.closed IS false AND gc.id IN %s AND (gg.state = 'inprogress' OR (gg.state = 'reached' AND (gg.end_date >= %s OR gg.end_date IS NULL))) GROUP BY gg.id """, [tuple(self.ids), yesterday]) Goals.browse(goal_id for [goal_id] in self.env.cr.fetchall()).update_goal() self._recompute_challenge_users() self._generate_goals_from_challenge() for challenge in self: if challenge.last_report_date != fields.Date.today(): # goals closed but still opened at the last report date closed_goals_to_report = Goals.search([ ('challenge_id', '=', challenge.id), ('start_date', '>=', challenge.last_report_date), ('end_date', '<=', challenge.last_report_date) ]) if challenge.next_report_date and fields.Date.today() >= challenge.next_report_date: challenge.report_progress() elif closed_goals_to_report: # some goals need a final report challenge.report_progress(subset_goals=closed_goals_to_report) self._check_challenge_reward() return True def _get_challenger_users(self, domain): # FIXME: literal_eval? user_domain = safe_eval(domain) return self.env['res.users'].search(user_domain) def _recompute_challenge_users(self): """Recompute the domain to add new users and remove the one no longer matching the domain""" for challenge in self.filtered(lambda c: c.user_domain): current_users = challenge.user_ids new_users = self._get_challenger_users(challenge.user_domain) if current_users != new_users: challenge.user_ids = new_users return True def action_start(self): """Start a challenge""" return self.write({'state': 'inprogress'}) def action_check(self): """Check a challenge Create goals that haven't been created yet (eg: if added users) Recompute the current value for each goal related""" self.env['gamification.goal'].search([ ('challenge_id', 'in', self.ids), ('state', '=', 'inprogress') ]).unlink() return self._update_all() def action_report_progress(self): """Manual report of a goal, does not influence automatic report frequency""" for challenge in self: challenge.report_progress() return True ##### Automatic actions ##### def _generate_goals_from_challenge(self): """Generate the goals for each line and user. If goals already exist for this line and user, the line is skipped. This can be called after each change in the list of users or lines. :param list(int) ids: the list of challenge concerned""" Goals = self.env['gamification.goal'] for challenge in self: (start_date, end_date) = start_end_date_for_period(challenge.period, challenge.start_date, challenge.end_date) to_update = Goals.browse(()) for line in challenge.line_ids: # there is potentially a lot of users # detect the ones with no goal linked to this line date_clause = "" query_params = [line.id] if start_date: date_clause += " AND g.start_date = %s" query_params.append(start_date) if end_date: date_clause += " AND g.end_date = %s" query_params.append(end_date) query = """SELECT u.id AS user_id FROM res_users u LEFT JOIN gamification_goal g ON (u.id = g.user_id) WHERE line_id = %s {date_clause} """.format(date_clause=date_clause) self.env.cr.execute(query, query_params) user_with_goal_ids = {it for [it] in self.env.cr._obj} participant_user_ids = set(challenge.user_ids.ids) user_squating_challenge_ids = user_with_goal_ids - participant_user_ids if user_squating_challenge_ids: # users that used to match the challenge Goals.search([ ('challenge_id', '=', challenge.id), ('user_id', 'in', list(user_squating_challenge_ids)) ]).unlink() values = { 'definition_id': line.definition_id.id, 'line_id': line.id, 'target_goal': line.target_goal, 'state': 'inprogress', } if start_date: values['start_date'] = start_date if end_date: values['end_date'] = end_date # the goal is initialised over the limit to make sure we will compute it at least once if line.condition == 'higher': values['current'] = min(line.target_goal - 1, 0) else: values['current'] = max(line.target_goal + 1, 0) if challenge.remind_update_delay: values['remind_update_delay'] = challenge.remind_update_delay for user_id in (participant_user_ids - user_with_goal_ids): values['user_id'] = user_id to_update |= Goals.create(values) to_update.update_goal() return True ##### JS utilities ##### def _get_serialized_challenge_lines(self, user=(), restrict_goals=(), restrict_top=0): """Return a serialised version of the goals information if the user has not completed every goal :param user: user retrieving progress (False if no distinction, only for ranking challenges) :param restrict_goals: compute only the results for this subset of gamification.goal ids, if False retrieve every goal of current running challenge :param int restrict_top: for challenge lines where visibility_mode is ``ranking``, retrieve only the best ``restrict_top`` results and itself, if 0 retrieve all restrict_goal_ids has priority over restrict_top format list # if visibility_mode == 'ranking' { 'name': <gamification.goal.description name>, 'description': <gamification.goal.description description>, 'condition': <reach condition {lower,higher}>, 'computation_mode': <target computation {manually,count,sum,python}>, 'monetary': <{True,False}>, 'suffix': <value suffix>, 'action': <{True,False}>, 'display_mode': <{progress,boolean}>, 'target': <challenge line target>, 'own_goal_id': <gamification.goal id where user_id == uid>, 'goals': [ { 'id': <gamification.goal id>, 'rank': <user ranking>, 'user_id': <res.users id>, 'name': <res.users name>, 'state': <gamification.goal state {draft,inprogress,reached,failed,canceled}>, 'completeness': <percentage>, 'current': <current value>, } ] }, # if visibility_mode == 'personal' { 'id': <gamification.goal id>, 'name': <gamification.goal.description name>, 'description': <gamification.goal.description description>, 'condition': <reach condition {lower,higher}>, 'computation_mode': <target computation {manually,count,sum,python}>, 'monetary': <{True,False}>, 'suffix': <value suffix>, 'action': <{True,False}>, 'display_mode': <{progress,boolean}>, 'target': <challenge line target>, 'state': <gamification.goal state {draft,inprogress,reached,failed,canceled}>, 'completeness': <percentage>, 'current': <current value>, } """ Goals = self.env['gamification.goal'] (start_date, end_date) = start_end_date_for_period(self.period) res_lines = [] for line in self.line_ids: line_data = { 'name': line.definition_id.name, 'description': line.definition_id.description, 'condition': line.definition_id.condition, 'computation_mode': line.definition_id.computation_mode, 'monetary': line.definition_id.monetary, 'suffix': line.definition_id.suffix, 'action': True if line.definition_id.action_id else False, 'display_mode': line.definition_id.display_mode, 'target': line.target_goal, } domain = [ ('line_id', '=', line.id), ('state', '!=', 'draft'), ] if restrict_goals: domain.append(('ids', 'in', restrict_goals.ids)) else: # if no subset goals, use the dates for restriction if start_date: domain.append(('start_date', '=', start_date)) if end_date: domain.append(('end_date', '=', end_date)) if self.visibility_mode == 'personal': if not user: raise exceptions.UserError(_("Retrieving progress for personal challenge without user information")) domain.append(('user_id', '=', user.id)) goal = Goals.search(domain, limit=1) if not goal: continue if goal.state != 'reached': return [] line_data.update(goal.read(['id', 'current', 'completeness', 'state'])[0]) res_lines.append(line_data) continue line_data['own_goal_id'] = False, line_data['goals'] = [] if line.condition=='higher': goals = Goals.search(domain, order="completeness desc, current desc") else: goals = Goals.search(domain, order="completeness desc, current asc") if not goals: continue for ranking, goal in enumerate(goals): if user and goal.user_id == user: line_data['own_goal_id'] = goal.id elif restrict_top and ranking > restrict_top: # not own goal and too low to be in top continue line_data['goals'].append({ 'id': goal.id, 'user_id': goal.user_id.id, 'name': goal.user_id.name, 'rank': ranking, 'current': goal.current, 'completeness': goal.completeness, 'state': goal.state, }) if len(goals) < 3: # display at least the top 3 in the results missing = 3 - len(goals) for ranking, mock_goal in enumerate([{'id': False, 'user_id': False, 'name': '', 'current': 0, 'completeness': 0, 'state': False}] * missing, start=len(goals)): mock_goal['rank'] = ranking line_data['goals'].append(mock_goal) res_lines.append(line_data) return res_lines ##### Reporting ##### def report_progress(self, users=(), subset_goals=False): """Post report about the progress of the goals :param users: users that are concerned by the report. If False, will send the report to every user concerned (goal users and group that receive a copy). Only used for challenge with a visibility mode set to 'personal'. :param subset_goals: goals to restrict the report """ challenge = self MailTemplates = self.env['mail.template'] if challenge.visibility_mode == 'ranking': lines_boards = challenge._get_serialized_challenge_lines(restrict_goals=subset_goals) body_html = MailTemplates.with_context(challenge_lines=lines_boards)._render_template(challenge.report_template_id.body_html, 'gamification.challenge', challenge.id) # send to every follower and participant of the challenge challenge.message_post( body=body_html, partner_ids=challenge.mapped('user_ids.partner_id.id'), subtype='mail.mt_comment', email_layout_xmlid='mail.mail_notification_light', ) if challenge.report_message_group_id: challenge.report_message_group_id.message_post( body=body_html, subtype='mail.mt_comment') else: # generate individual reports for user in (users or challenge.user_ids): lines = challenge._get_serialized_challenge_lines(user, restrict_goals=subset_goals) if not lines: continue body_html = MailTemplates.with_user(user).with_context(challenge_lines=lines)._render_template( challenge.report_template_id.body_html, 'gamification.challenge', challenge.id) # notify message only to users, do not post on the challenge challenge.message_notify( body=body_html, partner_ids=[user.partner_id.id], subtype='mail.mt_comment', email_layout_xmlid='mail.mail_notification_light', ) if challenge.report_message_group_id: challenge.report_message_group_id.message_post( body=body_html, subtype='mail.mt_comment', email_layout_xmlid='mail.mail_notification_light', ) return challenge.write({'last_report_date': fields.Date.today()}) ##### Challenges ##### def accept_challenge(self): user = self.env.user sudoed = self.sudo() sudoed.message_post(body=_("%s has joined the challenge") % user.name) sudoed.write({'invited_user_ids': [(3, user.id)], 'user_ids': [(4, user.id)]}) return sudoed._generate_goals_from_challenge() def discard_challenge(self): """The user discard the suggested challenge""" user = self.env.user sudoed = self.sudo() sudoed.message_post(body=_("%s has refused the challenge") % user.name) return sudoed.write({'invited_user_ids': (3, user.id)}) def _check_challenge_reward(self, force=False): """Actions for the end of a challenge If a reward was selected, grant it to the correct users. Rewards granted at: - the end date for a challenge with no periodicity - the end of a period for challenge with periodicity - when a challenge is manually closed (if no end date, a running challenge is never rewarded) """ commit = self.env.context.get('commit_gamification') and self.env.cr.commit for challenge in self: (start_date, end_date) = start_end_date_for_period(challenge.period, challenge.start_date, challenge.end_date) yesterday = date.today() - timedelta(days=1) rewarded_users = self.env['res.users'] challenge_ended = force or end_date == fields.Date.to_string(yesterday) if challenge.reward_id and (challenge_ended or challenge.reward_realtime): # not using start_date as intemportal goals have a start date but no end_date reached_goals = self.env['gamification.goal'].read_group([ ('challenge_id', '=', challenge.id), ('end_date', '=', end_date), ('state', '=', 'reached') ], fields=['user_id'], groupby=['user_id']) for reach_goals_user in reached_goals: if reach_goals_user['user_id_count'] == len(challenge.line_ids): # the user has succeeded every assigned goal user = self.env['res.users'].browse(reach_goals_user['user_id'][0]) if challenge.reward_realtime: badges = self.env['gamification.badge.user'].search_count([ ('challenge_id', '=', challenge.id), ('badge_id', '=', challenge.reward_id.id), ('user_id', '=', user.id), ]) if badges > 0: # has already recieved the badge for this challenge continue challenge._reward_user(user, challenge.reward_id) rewarded_users |= user if commit: commit() if challenge_ended: # open chatter message message_body = _("The challenge %s is finished.") % challenge.name if rewarded_users: user_names = rewarded_users.name_get() message_body += _("<br/>Reward (badge %s) for every succeeding user was sent to %s.") % (challenge.reward_id.name, ", ".join(name for (user_id, name) in user_names)) else: message_body += _("<br/>Nobody has succeeded to reach every goal, no badge is rewarded for this challenge.") # reward bests reward_message = _("<br/> %(rank)d. %(user_name)s - %(reward_name)s") if challenge.reward_first_id: (first_user, second_user, third_user) = challenge._get_topN_users(MAX_VISIBILITY_RANKING) if first_user: challenge._reward_user(first_user, challenge.reward_first_id) message_body += _("<br/>Special rewards were sent to the top competing users. The ranking for this challenge is :") message_body += reward_message % { 'rank': 1, 'user_name': first_user.name, 'reward_name': challenge.reward_first_id.name, } else: message_body += _("Nobody reached the required conditions to receive special badges.") if second_user and challenge.reward_second_id: challenge._reward_user(second_user, challenge.reward_second_id) message_body += reward_message % { 'rank': 2, 'user_name': second_user.name, 'reward_name': challenge.reward_second_id.name, } if third_user and challenge.reward_third_id: challenge._reward_user(third_user, challenge.reward_third_id) message_body += reward_message % { 'rank': 3, 'user_name': third_user.name, 'reward_name': challenge.reward_third_id.name, } challenge.message_post( partner_ids=[user.partner_id.id for user in challenge.user_ids], body=message_body) if commit: commit() return True def _get_topN_users(self, n): """Get the top N users for a defined challenge Ranking criterias: 1. succeed every goal of the challenge 2. total completeness of each goal (can be over 100) Only users having reached every goal of the challenge will be returned unless the challenge ``reward_failure`` is set, in which case any user may be considered. :returns: an iterable of exactly N records, either User objects or False if there was no user for the rank. There can be no False between two users (if users[k] = False then users[k+1] = False """ Goals = self.env['gamification.goal'] (start_date, end_date) = start_end_date_for_period(self.period, self.start_date, self.end_date) challengers = [] for user in self.user_ids: all_reached = True total_completeness = 0 # every goal of the user for the running period goal_ids = Goals.search([ ('challenge_id', '=', self.id), ('user_id', '=', user.id), ('start_date', '=', start_date), ('end_date', '=', end_date) ]) for goal in goal_ids: if goal.state != 'reached': all_reached = False if goal.definition_condition == 'higher': # can be over 100 total_completeness += (100.0 * goal.current / goal.target_goal) if goal.target_goal else 0 elif goal.state == 'reached': # for lower goals, can not get percentage so 0 or 100 total_completeness += 100 challengers.append({'user': user, 'all_reached': all_reached, 'total_completeness': total_completeness}) challengers.sort(key=lambda k: (k['all_reached'], k['total_completeness']), reverse=True) if not self.reward_failure: # only keep the fully successful challengers at the front, could # probably use filter since the successful ones are at the front challengers = itertools.takewhile(lambda c: c['all_reached'], challengers) # append a tail of False, then keep the first N challengers = itertools.islice( itertools.chain( (c['user'] for c in challengers), itertools.repeat(False), ), 0, n ) return tuple(challengers) def _reward_user(self, user, badge): """Create a badge user and send the badge to him :param user: the user to reward :param badge: the concerned badge """ return self.env['gamification.badge.user'].create({ 'user_id': user.id, 'badge_id': badge.id, 'challenge_id': self.id })._send_badge()
class SurveyQuestion(models.Model): """ Questions that will be asked in a survey. Each question can have one of more suggested answers (eg. in case of dropdown choices, multi-answer checkboxes, radio buttons...). Technical note: survey.question is also the model used for the survey's pages (with the "is_page" field set to True). A page corresponds to a "section" in the interface, and the fact that it separates the survey in actual pages in the interface depends on the "questions_layout" parameter on the survey.survey model. Pages are also used when randomizing questions. The randomization can happen within a "page". Using the same model for questions and pages allows to put all the pages and questions together in a o2m field (see survey.survey.question_and_page_ids) on the view side and easily reorganize your survey by dragging the items around. It also removes on level of encoding by directly having 'Add a page' and 'Add a question' links on the tree view of questions, enabling a faster encoding. However, this has the downside of making the code reading a little bit more complicated. Efforts were made at the model level to create computed fields so that the use of these models still seems somewhat logical. That means: - A survey still has "page_ids" (question_and_page_ids filtered on is_page = True) - These "page_ids" still have question_ids (questions located between this page and the next) - These "question_ids" still have a "page_id" That makes the use and display of these information at view and controller levels easier to understand. """ _name = 'survey.question' _description = 'Survey Question' _rec_name = 'question' _order = 'sequence,id' @api.model def default_get(self, fields): defaults = super(SurveyQuestion, self).default_get(fields) if (not fields or 'question_type' in fields): defaults['question_type'] = False if defaults.get( 'is_page') == True else 'free_text' return defaults # Question metadata survey_id = fields.Many2one('survey.survey', string='Survey', ondelete='cascade') page_id = fields.Many2one('survey.question', string='Page', compute="_compute_page_id", store=True) question_ids = fields.One2many('survey.question', string='Questions', compute="_compute_question_ids") scoring_type = fields.Selection(related='survey_id.scoring_type', string='Scoring Type', readonly=True) sequence = fields.Integer('Sequence', default=10) # Question is_page = fields.Boolean('Is a page?') questions_selection = fields.Selection( related='survey_id.questions_selection', readonly=True, help= "If randomized is selected, add the number of random questions next to the section." ) random_questions_count = fields.Integer( 'Random questions count', default=1, help= "Used on randomized sections to take X random questions from all the questions of that section." ) title = fields.Char('Title', required=True, translate=True) question = fields.Char('Question', related="title") description = fields.Html( 'Description', help= "Use this field to add additional explanations about your question", translate=True) question_type = fields.Selection( [('free_text', 'Multiple Lines Text Box'), ('textbox', 'Single Line Text Box'), ('numerical_box', 'Numerical Value'), ('date', 'Date'), ('datetime', 'Datetime'), ('simple_choice', 'Multiple choice: only one answer'), ('multiple_choice', 'Multiple choice: multiple answers allowed'), ('matrix', 'Matrix')], string='Question Type') # simple choice / multiple choice / matrix labels_ids = fields.One2many( 'survey.label', 'question_id', string='Types of answers', copy=True, help= 'Labels used for proposed choices: simple choice, multiple choice and columns of matrix' ) # matrix matrix_subtype = fields.Selection( [('simple', 'One choice per row'), ('multiple', 'Multiple choices per row')], string='Matrix Type', default='simple') labels_ids_2 = fields.One2many( 'survey.label', 'question_id_2', string='Rows of the Matrix', copy=True, help='Labels used for proposed choices: rows of matrix') # Display options column_nb = fields.Selection( [('12', '1'), ('6', '2'), ('4', '3'), ('3', '4'), ('2', '6')], string='Number of columns', default='12', help= 'These options refer to col-xx-[12|6|4|3|2] classes in Bootstrap for dropdown-based simple and multiple choice questions.' ) display_mode = fields.Selection( [('columns', 'Radio Buttons'), ('dropdown', 'Selection Box')], string='Display Mode', default='columns', help='Display mode of simple choice questions.') # Comments comments_allowed = fields.Boolean('Show Comments Field') comments_message = fields.Char( 'Comment Message', translate=True, default=lambda self: _("If other, please specify:")) comment_count_as_answer = fields.Boolean( 'Comment Field is an Answer Choice') # Validation validation_required = fields.Boolean('Validate entry') validation_email = fields.Boolean('Input must be an email') validation_length_min = fields.Integer('Minimum Text Length') validation_length_max = fields.Integer('Maximum Text Length') validation_min_float_value = fields.Float('Minimum value') validation_max_float_value = fields.Float('Maximum value') validation_min_date = fields.Date('Minimum Date') validation_max_date = fields.Date('Maximum Date') validation_min_datetime = fields.Datetime('Minimum Datetime') validation_max_datetime = fields.Datetime('Maximum Datetime') validation_error_msg = fields.Char( 'Validation Error message', translate=True, default=lambda self: _("The answer you entered is not valid.")) # Constraints on number of answers (matrices) constr_mandatory = fields.Boolean('Mandatory Answer') constr_error_msg = fields.Char( 'Error message', translate=True, default=lambda self: _("This question requires an answer.")) # Answer user_input_line_ids = fields.One2many('survey.user_input_line', 'question_id', string='Answers', domain=[('skipped', '=', False)], groups='survey.group_survey_user') _sql_constraints = [ ('positive_len_min', 'CHECK (validation_length_min >= 0)', 'A length must be positive!'), ('positive_len_max', 'CHECK (validation_length_max >= 0)', 'A length must be positive!'), ('validation_length', 'CHECK (validation_length_min <= validation_length_max)', 'Max length cannot be smaller than min length!'), ('validation_float', 'CHECK (validation_min_float_value <= validation_max_float_value)', 'Max value cannot be smaller than min value!'), ('validation_date', 'CHECK (validation_min_date <= validation_max_date)', 'Max date cannot be smaller than min date!'), ('validation_datetime', 'CHECK (validation_min_datetime <= validation_max_datetime)', 'Max datetime cannot be smaller than min datetime!') ] @api.onchange('validation_email') def _onchange_validation_email(self): if self.validation_email: self.validation_required = False @api.onchange('is_page') def _onchange_is_page(self): if self.is_page: self.question_type = False # Validation methods def validate_question(self, post, answer_tag): """ Validate question, depending on question type and parameters """ self.ensure_one() try: checker = getattr(self, 'validate_' + self.question_type) except AttributeError: _logger.warning(self.question_type + ": This type of question has no validation method") return {} else: return checker(post, answer_tag) def validate_free_text(self, post, answer_tag): self.ensure_one() errors = {} answer = post[answer_tag].strip() # Empty answer to mandatory question if self.constr_mandatory and not answer: errors.update({answer_tag: self.constr_error_msg}) return errors def validate_textbox(self, post, answer_tag): self.ensure_one() errors = {} answer = post[answer_tag].strip() # Empty answer to mandatory question if self.constr_mandatory and not answer: errors.update({answer_tag: self.constr_error_msg}) # Email format validation # Note: this validation is very basic: # all the strings of the form # <something>@<anything>.<extension> # will be accepted if answer and self.validation_email: if not email_validator.match(answer): errors.update( {answer_tag: _('This answer must be an email address')}) # Answer validation (if properly defined) # Length of the answer must be in a range if answer and self.validation_required: if not (self.validation_length_min <= len(answer) <= self.validation_length_max): errors.update({answer_tag: self.validation_error_msg}) return errors def validate_numerical_box(self, post, answer_tag): self.ensure_one() errors = {} answer = post[answer_tag].strip() # Empty answer to mandatory question if self.constr_mandatory and not answer: errors.update({answer_tag: self.constr_error_msg}) # Checks if user input is a number if answer: try: floatanswer = float(answer) except ValueError: errors.update({answer_tag: _('This is not a number')}) # Answer validation (if properly defined) if answer and self.validation_required: # Answer is not in the right range with tools.ignore(Exception): floatanswer = float( answer) # check that it is a float has been done hereunder if not (self.validation_min_float_value <= floatanswer <= self.validation_max_float_value): errors.update({answer_tag: self.validation_error_msg}) return errors def date_validation(self, date_type, post, answer_tag, min_value, max_value): self.ensure_one() errors = {} if date_type not in ('date', 'datetime'): raise ValueError("Unexpected date type value") answer = post[answer_tag].strip() # Empty answer to mandatory question if self.constr_mandatory and not answer: errors.update({answer_tag: self.constr_error_msg}) # Checks if user input is a date if answer: try: if date_type == 'datetime': dateanswer = fields.Datetime.from_string(answer) else: dateanswer = fields.Date.from_string(answer) except ValueError: errors.update({answer_tag: _('This is not a date')}) return errors # Answer validation (if properly defined) if answer and self.validation_required: # Answer is not in the right range try: if date_type == 'datetime': date_from_string = fields.Datetime.from_string else: date_from_string = fields.Date.from_string dateanswer = date_from_string(answer) min_date = date_from_string(min_value) max_date = date_from_string(max_value) if min_date and max_date and not (min_date <= dateanswer <= max_date): # If Minimum and Maximum Date are entered errors.update({answer_tag: self.validation_error_msg}) elif min_date and not min_date <= dateanswer: # If only Minimum Date is entered and not Define Maximum Date errors.update({answer_tag: self.validation_error_msg}) elif max_date and not dateanswer <= max_date: # If only Maximum Date is entered and not Define Minimum Date errors.update({answer_tag: self.validation_error_msg}) except ValueError: # check that it is a date has been done hereunder pass return errors def validate_date(self, post, answer_tag): return self.date_validation('date', post, answer_tag, self.validation_min_date, self.validation_max_date) def validate_datetime(self, post, answer_tag): return self.date_validation('datetime', post, answer_tag, self.validation_min_datetime, self.validation_max_datetime) def validate_simple_choice(self, post, answer_tag): self.ensure_one() errors = {} if self.comments_allowed: comment_tag = "%s_%s" % (answer_tag, 'comment') # Empty answer to mandatory self if self.constr_mandatory and answer_tag not in post: errors.update({answer_tag: self.constr_error_msg}) if self.constr_mandatory and answer_tag in post and not post[ answer_tag].strip(): errors.update({answer_tag: self.constr_error_msg}) # Answer is a comment and is empty if self.constr_mandatory and answer_tag in post and post[ answer_tag] == "-1" and self.comment_count_as_answer and comment_tag in post and not post[ comment_tag].strip(): errors.update({answer_tag: self.constr_error_msg}) return errors def validate_multiple_choice(self, post, answer_tag): self.ensure_one() errors = {} if self.constr_mandatory: answer_candidates = dict_keys_startswith(post, answer_tag) comment_flag = answer_candidates.pop(("%s_%s" % (answer_tag, -1)), None) if self.comments_allowed: comment_answer = answer_candidates.pop( ("%s_%s" % (answer_tag, 'comment')), '').strip() # Preventing answers with blank value if all(not answer.strip() for answer in answer_candidates.values()) and answer_candidates: errors.update({answer_tag: self.constr_error_msg}) # There is no answer neither comments (if comments count as answer) if not answer_candidates and self.comment_count_as_answer and ( not comment_flag or not comment_answer): errors.update({answer_tag: self.constr_error_msg}) # There is no answer at all if not answer_candidates and not self.comment_count_as_answer: errors.update({answer_tag: self.constr_error_msg}) return errors def validate_matrix(self, post, answer_tag): self.ensure_one() errors = {} if self.constr_mandatory: lines_number = len(self.labels_ids_2) answer_candidates = dict_keys_startswith(post, answer_tag) answer_candidates.pop(("%s_%s" % (answer_tag, 'comment')), '').strip() # Number of lines that have been answered if self.matrix_subtype == 'simple': answer_number = len(answer_candidates) elif self.matrix_subtype == 'multiple': answer_number = len( {sk.rsplit('_', 1)[0] for sk in answer_candidates}) else: raise RuntimeError("Invalid matrix subtype") # Validate that each line has been answered if answer_number != lines_number: errors.update({answer_tag: self.constr_error_msg}) return errors @api.depends('survey_id.question_and_page_ids.is_page', 'survey_id.question_and_page_ids.sequence') def _compute_question_ids(self): """Will take all questions of the survey for which the index is higher than the index of this page and lower than the index of the next page.""" for question in self: if question.is_page: next_page_index = False for page in question.survey_id.page_ids: if page._index() > question._index(): next_page_index = page._index() break question.question_ids = question.survey_id.question_ids.filtered( lambda q: q._index() > question._index() and (not next_page_index or q._index() < next_page_index)) else: question.question_ids = self.env['survey.question'] @api.depends('survey_id.question_and_page_ids.is_page', 'survey_id.question_and_page_ids.sequence') def _compute_page_id(self): """Will find the page to which this question belongs to by looking inside the corresponding survey""" for question in self: if question.is_page: question.page_id = None else: question.page_id = next((iter( question.survey_id.question_and_page_ids.filtered( lambda q: q.is_page and q.sequence < question.sequence ).sorted(reverse=True))), None) def _index(self): """We would normally just use the 'sequence' field of questions BUT, if the pages and questions are created without ever moving records around, the sequence field can be set to 0 for all the questions. However, the order of the recordset is always correct so we can rely on the index method.""" self.ensure_one() return list(self.survey_id.question_and_page_ids).index(self) def get_correct_answer_ids(self): self.ensure_one() return self.labels_ids.filtered(lambda label: label.is_correct)
class MrpProduction(models.Model): _inherit = 'mrp.production' extra_cost = fields.Float(copy=False, help='Extra cost per produced unit') show_valuation = fields.Boolean(compute='_compute_show_valuation') def _compute_show_valuation(self): for order in self: order.show_valuation = any(m.state == 'done' for m in order.move_finished_ids) def _cal_price(self, consumed_moves): """Set a price unit on the finished move according to `consumed_moves`. """ super(MrpProduction, self)._cal_price(consumed_moves) work_center_cost = 0 finished_move = self.move_finished_ids.filtered( lambda x: x.product_id == self.product_id and x.state not in ('done', 'cancel') and x.quantity_done > 0) if finished_move: finished_move.ensure_one() for work_order in self.workorder_ids: time_lines = work_order.time_ids.filtered( lambda x: x.date_end and not x.cost_already_recorded) duration = sum(time_lines.mapped('duration')) time_lines.write({'cost_already_recorded': True}) work_center_cost += ( duration / 60.0) * work_order.workcenter_id.costs_hour if finished_move.product_id.cost_method in ('fifo', 'average'): qty_done = finished_move.product_uom._compute_quantity( finished_move.quantity_done, finished_move.product_id.uom_id) extra_cost = self.extra_cost * qty_done finished_move.price_unit = (sum([ -m.stock_valuation_layer_ids.value for m in consumed_moves ]) + work_center_cost + extra_cost) / qty_done return True def _prepare_wc_analytic_line(self, wc_line): wc = wc_line.workcenter_id hours = wc_line.duration / 60.0 value = hours * wc.costs_hour account = wc.costs_hour_account_id.id return { 'name': wc_line.name + ' (H)', 'amount': -value, 'account_id': account, 'ref': wc.code, 'unit_amount': hours, 'company_id': self.company_id.id, } def _costs_generate(self): """ Calculates total costs at the end of the production. """ self.ensure_one() AccountAnalyticLine = self.env['account.analytic.line'].sudo() for wc_line in self.workorder_ids.filtered( 'workcenter_id.costs_hour_account_id'): vals = self._prepare_wc_analytic_line(wc_line) precision_rounding = wc_line.workcenter_id.costs_hour_account_id.currency_id.rounding if not float_is_zero(vals.get('amount', 0.0), precision_rounding=precision_rounding): # we use SUPERUSER_ID as we do not guarantee an mrp user # has access to account analytic lines but still should be # able to produce orders AccountAnalyticLine.create(vals) def button_mark_done(self): self.ensure_one() res = super(MrpProduction, self).button_mark_done() self._costs_generate() return res def action_view_stock_valuation_layers(self): self.ensure_one() domain = [('id', 'in', (self.move_raw_ids + self.move_finished_ids + self.scrap_ids.move_id).stock_valuation_layer_ids.ids)] action = self.env.ref( 'stock_account.stock_valuation_layer_action').read()[0] context = literal_eval(action['context']) context.update(self.env.context) context['no_at_date'] = True return dict(action, domain=domain, context=context)
class EventType(models.Model): _name = 'event.type' _inherit = ['event.type'] website_menu = fields.Boolean('Display a dedicated menu on Website')
class SaasClient(models.Model): _name = 'saas.client' _order = 'id desc' _description = 'Class for managing SaaS Instances(Clients)' @api.depends('data_directory_path') def _compute_addons_path(self): for obj in self: if obj.data_directory_path and type(obj.id) != NewId: obj.addons_path = "{}/addons/13.0".format( obj.data_directory_path) else: obj.addons_path = "" name = fields.Char(string="Name") client_url = fields.Char(string="URL") database_name = fields.Char(string="Database Name") saas_contract_id = fields.Many2one(comodel_name="saas.contract", string="SaaS Contract") partner_id = fields.Many2one(comodel_name="res.partner", string="Customer") containter_port = fields.Char(string="Port") containter_path = fields.Char(string="Path") container_name = fields.Char(string="Instance Name") container_id = fields.Char(string="Instance ID") data_directory_path = fields.Char(string="Data Directory Path") addons_path = fields.Char(compute='_compute_addons_path', string="Extra Addons Path") saas_module_ids = fields.One2many(comodel_name="saas.module.status", inverse_name="client_id", string="Related Modules") server_id = fields.Many2one(comodel_name="saas.server", string="SaaS Server") invitation_url = fields.Char("Invitation URL") state = fields.Selection(selection=CLIENT_STATE, default="draft", string="State") is_drop_db = fields.Boolean(string="Drop Db", default=False) is_drop_container = fields.Boolean(string="Drop Container", default=False) _sql_constraints = [ ('database_name_uniq', 'unique(database_name)', 'Database Name Must Be Unique !!!'), ] @api.model def create_docker_instance(self, domain_name=None): modules = [module.technical_name for module in self.saas_module_ids] host_server, db_server = self.saas_contract_id.plan_id.server_id.get_server_details( ) response = None self.database_name = domain_name.replace("https://", "").replace("http://", "") config_path = get_module_resource('eagle_saas_kit') response = saas.main( dict(db_template=self.saas_contract_id.db_template, db_name=self.database_name, modules=modules, config_path=config_path, host_domain=domain_name, host_server=host_server, db_server=db_server)) return response @api.model def create_client_instance(self, domain_name=None): server_id = self.server_id if server_id.server_type == 'containerized': return self.create_docker_instance(domain_name) return False def fetch_client_url(self, domain_name=None): for obj in self: if type(domain_name) != str: if obj.saas_contract_id.use_separate_domain: domain_name = obj.saas_contract_id.domain_name else: domain_name = "{}.{}".format( obj.saas_contract_id.domain_name, obj.saas_contract_id.saas_domain_url) response = None try: response = obj.create_client_instance(domain_name) except Exception as e: raise UserError("Unable To Create Client\nERROR: {}".format(e)) if response: obj.client_url = response.get("url", False) obj.containter_port = response.get("port", False) obj.containter_path = response.get("path", False) obj.container_name = response.get("name", False) obj.container_id = response.get("container_id", False) obj.state = "started" obj.data_directory_path = response.get("extra-addons", False) if response.get("modules_installation", False): for module_status_id in obj.saas_module_ids: module_status_id.status = 'installed' else: for module_status_id in obj.saas_module_ids: if module_status_id.technical_name not in response.get( "modules_missed", []): module_status_id.status = 'installed' else: raise UserError( "Couldn't create the instance with the selected domain name. Please use some other domain name." ) def login_to_client_instance(self): for obj in self: host_server, db_server = obj.saas_contract_id.plan_id.server_id.get_server_details( ) response = query.get_credentials(obj.database_name, host_server=host_server, db_server=db_server) if response: login = response[0][0] password = response[0][1] login_url = "{}/saas/login?db={}&login={}&passwd={}".format( obj.client_url, obj.database_name, login, password) return { 'type': 'ir.actions.act_url', 'url': login_url, 'target': 'new', } else: raise UserError("Unknown Error!") def stop_client(self): for obj in self: host_server, db_server = obj.saas_contract_id.plan_id.server_id.get_server_details( ) response_flag = containers.action(operation="stop", container_id=obj.container_id, host_server=host_server, db_server=db_server) if response_flag: obj.state = "stopped" else: raise UserError("Operation Failed! Unknown Error!") def start_client(self): for obj in self: host_server, db_server = obj.saas_contract_id.plan_id.server_id.get_server_details( ) response_flag = containers.action(operation="start", container_id=obj.container_id, host_server=host_server, db_server=db_server) if response_flag: obj.state = "started" else: raise UserError("Operation Failed! Unknown Error!") def restart_client(self): for obj in self: host_server, db_server = obj.saas_contract_id.plan_id.server_id.get_server_details( ) response_flag = containers.action(operation="restart", container_id=obj.container_id, host_server=host_server, db_server=db_server) if response_flag: obj.state = "started" else: raise UserError("Operation Failed! Unknown Error!") @api.model def create(self, vals): vals['name'] = self.env['ir.sequence'].next_by_code('saas.client') return super(SaasClient, self).create(vals) def disable_client_wizard(self): raise UserError("Developement Under Process") res_wizard = self.env['saas.client.disable'].sudo().create( {'client_id': self.id}) return { 'name': 'Disable Client', 'view_mode': 'form', 'res_model': 'saas.client.disable', 'type': 'ir.actions.act_window', 'res_id': res_wizard.id, } def inactive_client(self): for obj in self: if obj.saas_contract_id.state != 'inactive': raise UserError('Please Inactive the Related Contract First') if obj.state in ['stopped', 'draft']: obj.state = 'inactive' else: raise UserError("Can't Inactive a Running Client") def unlink(self): for obj in self: raise Warning("Can't Delete Clients") def drop_db(self): for obj in self: if obj.state == "inactive": host_server, db_server = obj.saas_contract_id.plan_id.server_id.get_server_details( ) _logger.info("HOST SERER %r DB SERVER %r" % (host_server, db_server)) response = client.main(obj.database_name, obj.containter_port, host_server, get_module_resource('eagle_saas_kit'), from_drop_db=True) if not response['db_drop']: raise UserError( "ERROR: Couldn't Drop Client Database. Please Try Again Later.\n\nOperation\tStatus\n\nDrop database: \t{}\n" .format(response['db_drop'])) else: obj.is_drop_db = True def drop_container(self): for obj in self: if obj.state == "inactive": host_server, db_server = obj.saas_contract_id.plan_id.server_id.get_server_details( ) _logger.info("HOST SERER %r DB SERVER %r" % (host_server, db_server)) response = client.main(obj.database_name, obj.containter_port, host_server, get_module_resource('eagle_saas_kit'), container_id=obj.container_id, db_server=db_server, from_drop_container=True) if not response['drop_container'] or not response[ 'delete_nginx_vhost'] or not response[ 'delete_data_dir']: raise UserError( "ERROR: Couldn't Drop Client Container. Please Try Again Later.\n\nOperation\tStatus\n\nDelete Domain Mapping: \t{}\nDelete Data Directory: \t{}" .format(response['drop_container'], response['delete_nginx_vhost'])) else: obj.is_drop_container = True
class Event(models.Model): _name = 'event.event' _inherit = [ 'event.event', 'website.seo.metadata', 'website.published.multi.mixin' ] is_published = fields.Boolean(track_visibility='onchange') is_participating = fields.Boolean("Is Participating", compute="_compute_is_participating") website_menu = fields.Boolean( 'Dedicated Menu', help="Creates menus Introduction, Location and Register on the page " " of the event on the website.", copy=False) 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['website'].get_current_website().user_id: 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_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), ] def _toggle_create_website_menus(self, vals): for event in self: if 'website_menu' in vals: 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, 'website_id': event.website_id.id }) event.menu_id = root_menu for sequence, (name, url, xml_id) in enumerate( event._get_menu_entries()): event._create_menu(sequence, name, url, xml_id) @api.model def create(self, vals): res = super(Event, self).create(vals) res._toggle_create_website_menus(vals) return res @api.multi def write(self, vals): res = super(Event, self).write(vals) self._toggle_create_website_menus(vals) return res def _create_menu(self, sequence, name, url, xml_id): if not url: newpath = self.env['website'].new_page(name + ' ' + self.name, template=xml_id, ispage=False)['url'] url = "/event/" + slug(self) + "/page/" + newpath[1:] menu = self.env['website.menu'].create({ 'name': name, 'url': url, 'parent_id': self.menu_id.id, 'sequence': sequence, 'website_id': self.website_id.id, }) return menu @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 'is_published' in init_values and self.is_published: return 'website_event.mt_event_published' elif 'is_published' in init_values and not self.is_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), } @api.multi def _get_ics_file(self): """ Returns iCalendar file for the event invitation. :returns a dict of .ics file content for each event """ result = {} if not vobject: return result for event in self: cal = vobject.iCalendar() cal_event = cal.add('vevent') if not event.date_begin or not event.date_end: raise UserError( _("No date has been specified for the event, no file will be generated." )) cal_event.add('created').value = fields.Datetime.now().replace( tzinfo=pytz.timezone('UTC')) cal_event.add('dtstart').value = fields.Datetime.from_string( event.date_begin).replace(tzinfo=pytz.timezone('UTC')) cal_event.add('dtend').value = fields.Datetime.from_string( event.date_end).replace(tzinfo=pytz.timezone('UTC')) cal_event.add('summary').value = event.name if event.address_id: cal_event.add( 'location').value = event.sudo().address_id.contact_address result[event.id] = cal.serialize().encode('utf-8') return result def _get_event_resource_urls(self, attendees): url_date_start = self.date_begin.strftime('%Y%m%dT%H%M%SZ') url_date_stop = self.date_end.strftime('%Y%m%dT%H%M%SZ') params = { 'action': 'TEMPLATE', 'text': self.name, 'dates': url_date_start + '/' + url_date_stop, 'details': self.name, } if self.address_id: params.update( location=self.sudo().address_id.contact_address.replace( '\n', ' ')) encoded_params = werkzeug.url_encode(params) google_url = GOOGLE_CALENDAR_URL + encoded_params iCal_url = '/event/%s/ics?%s' % (slug(self), encoded_params) return {'google_url': google_url, 'iCal_url': iCal_url} def _default_website_meta(self): res = super(Event, self)._default_website_meta() res['default_opengraph']['og:title'] = res['default_twitter'][ 'twitter:title'] = self.name res['default_opengraph']['og:description'] = res['default_twitter'][ 'twitter:description'] = self.date_begin res['default_twitter']['twitter:card'] = 'summary' return res
class MrpBom(models.Model): """ Defines bills of material for a product or a product template """ _name = 'mrp.bom' _description = 'Bill of Material' _inherit = ['mail.thread'] _rec_name = 'product_tmpl_id' _order = "sequence" _check_company_auto = True def _get_default_product_uom_id(self): return self.env['uom.uom'].search([], limit=1, order='id').id code = fields.Char('Reference') active = fields.Boolean( 'Active', default=True, help= "If the active field is set to False, it will allow you to hide the bills of material without removing it." ) type = fields.Selection([('normal', 'Manufacture this product'), ('phantom', 'Kit')], 'BoM Type', default='normal', required=True) product_tmpl_id = fields.Many2one( 'product.template', 'Product', check_company=True, domain= "[('type', 'in', ['product', 'consu']), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", required=True) product_id = fields.Many2one( 'product.product', 'Product Variant', check_company=True, domain= "['&', ('product_tmpl_id', '=', product_tmpl_id), ('type', 'in', ['product', 'consu']), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", help= "If a product variant is defined the BOM is available only for this product." ) bom_line_ids = fields.One2many('mrp.bom.line', 'bom_id', 'BoM Lines', copy=True) byproduct_ids = fields.One2many('mrp.bom.byproduct', 'bom_id', 'By-products', copy=True) product_qty = fields.Float('Quantity', default=1.0, digits='Unit of Measure', required=True) product_uom_id = fields.Many2one( 'uom.uom', 'Unit of Measure', default=_get_default_product_uom_id, required=True, help= "Unit of Measure (Unit of Measure) is the unit of measurement for the inventory control", domain="[('category_id', '=', product_uom_category_id)]") product_uom_category_id = fields.Many2one( related='product_id.uom_id.category_id') sequence = fields.Integer( 'Sequence', help= "Gives the sequence order when displaying a list of bills of material." ) routing_id = fields.Many2one( 'mrp.routing', 'Routing', check_company=True, help= "The operations for producing this BoM. When a routing is specified, the production orders will " " be executed through work orders, otherwise everything is processed in the production order itself. " ) ready_to_produce = fields.Selection( [('all_available', ' When all components are available'), ('asap', 'When components for 1st operation are available')], string='Manufacturing Readiness', default='asap', help= "Defines when a Manufacturing Order is considered as ready to be started", required=True) picking_type_id = fields.Many2one( 'stock.picking.type', 'Operation Type', domain= "[('code', '=', 'mrp_operation'), ('company_id', '=', company_id)]", check_company=True, help= u"When a procurement has a ‘produce’ route with a operation type set, it will try to create " "a Manufacturing Order for that product using a BoM of the same operation type. That allows " "to define stock rules which trigger different manufacturing orders with different BoMs." ) company_id = fields.Many2one('res.company', 'Company', index=True, default=lambda self: self.env.company) consumption = fields.Selection( [('strict', 'Strict'), ('flexible', 'Flexible')], help= "Defines if you can consume more or less components than the quantity defined on the BoM.", default='strict', string='Consumption') @api.onchange('product_id') def onchange_product_id(self): if self.product_id: for line in self.bom_line_ids: line.bom_product_template_attribute_value_ids = False @api.constrains('product_id', 'product_tmpl_id', 'bom_line_ids') def _check_bom_lines(self): for bom in self: for bom_line in bom.bom_line_ids: if bom.product_id and bom_line.product_id == bom.product_id: raise ValidationError( _("BoM line product %s should not be the same as BoM product." ) % bom.display_name) if bom_line.product_tmpl_id == bom.product_tmpl_id: raise ValidationError( _("BoM line product %s should not be the same as BoM product." ) % bom.display_name) if bom.product_id and bom_line.bom_product_template_attribute_value_ids: raise ValidationError( _("BoM cannot concern product %s and have a line with attributes (%s) at the same time." ) % (bom.product_id.display_name, ", ".join([ ptav.display_name for ptav in bom_line.bom_product_template_attribute_value_ids ]))) for ptav in bom_line.bom_product_template_attribute_value_ids: if ptav.product_tmpl_id != bom.product_tmpl_id: raise ValidationError( _("The attribute value %s set on product %s does not match the BoM product %s." ) % (ptav.display_name, ptav.product_tmpl_id.display_name, bom_line.parent_product_tmpl_id.display_name)) @api.onchange('product_uom_id') def onchange_product_uom_id(self): res = {} if not self.product_uom_id or not self.product_tmpl_id: return if self.product_uom_id.category_id.id != self.product_tmpl_id.uom_id.category_id.id: self.product_uom_id = self.product_tmpl_id.uom_id.id res['warning'] = { 'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.' ) } return res @api.onchange('product_tmpl_id') def onchange_product_tmpl_id(self): if self.product_tmpl_id: self.product_uom_id = self.product_tmpl_id.uom_id.id if self.product_id.product_tmpl_id != self.product_tmpl_id: self.product_id = False for line in self.bom_line_ids: line.bom_product_template_attribute_value_ids = False @api.onchange('routing_id') def onchange_routing_id(self): for line in self.bom_line_ids: line.operation_id = False @api.model def name_create(self, name): # prevent to use string as product_tmpl_id if isinstance(name, str): raise UserError( _("You cannot create a new Bill of Material from here.")) return super(MrpBom, self).name_create(name) def name_get(self): return [(bom.id, '%s%s' % (bom.code and '%s: ' % bom.code or '', bom.product_tmpl_id.display_name)) for bom in self] def unlink(self): if self.env['mrp.production'].search( [('bom_id', 'in', self.ids), ('state', 'not in', ['done', 'cancel'])], limit=1): raise UserError( _('You can not delete a Bill of Material with running manufacturing orders.\nPlease close or cancel it first.' )) return super(MrpBom, self).unlink() @api.model def _bom_find_domain(self, product_tmpl=None, product=None, picking_type=None, company_id=False, bom_type=False): if product: if not product_tmpl: product_tmpl = product.product_tmpl_id domain = [ '|', ('product_id', '=', product.id), '&', ('product_id', '=', False), ('product_tmpl_id', '=', product_tmpl.id) ] elif product_tmpl: domain = [('product_tmpl_id', '=', product_tmpl.id)] else: # neither product nor template, makes no sense to search raise UserError( _('You should provide either a product or a product template to search a BoM' )) if picking_type: domain += [ '|', ('picking_type_id', '=', picking_type.id), ('picking_type_id', '=', False) ] if company_id or self.env.context.get('company_id'): domain = domain + [ '|', ('company_id', '=', False), ('company_id', '=', company_id or self.env.context.get('company_id')) ] if bom_type: domain += [('type', '=', bom_type)] # order to prioritize bom with product_id over the one without return domain @api.model def _bom_find(self, product_tmpl=None, product=None, picking_type=None, company_id=False, bom_type=False): """ Finds BoM for particular product, picking and company """ if product and product.type == 'service' or product_tmpl and product_tmpl.type == 'service': return False domain = self._bom_find_domain(product_tmpl=product_tmpl, product=product, picking_type=picking_type, company_id=company_id, bom_type=bom_type) if domain is False: return domain return self.search(domain, order='sequence, product_id', limit=1) def explode(self, product, quantity, picking_type=False): """ Explodes the BoM and creates two lists with all the information you need: bom_done and line_done Quantity describes the number of times you need the BoM: so the quantity divided by the number created by the BoM and converted into its UoM """ from collections import defaultdict graph = defaultdict(list) V = set() def check_cycle(v, visited, recStack, graph): visited[v] = True recStack[v] = True for neighbour in graph[v]: if visited[neighbour] == False: if check_cycle(neighbour, visited, recStack, graph) == True: return True elif recStack[neighbour] == True: return True recStack[v] = False return False boms_done = [(self, { 'qty': quantity, 'product': product, 'original_qty': quantity, 'parent_line': False })] lines_done = [] V |= set([product.product_tmpl_id.id]) bom_lines = [(bom_line, product, quantity, False) for bom_line in self.bom_line_ids] for bom_line in self.bom_line_ids: V |= set([bom_line.product_id.product_tmpl_id.id]) graph[product.product_tmpl_id.id].append( bom_line.product_id.product_tmpl_id.id) while bom_lines: current_line, current_product, current_qty, parent_line = bom_lines[ 0] bom_lines = bom_lines[1:] if current_line._skip_bom_line(current_product): continue line_quantity = current_qty * current_line.product_qty bom = self._bom_find(product=current_line.product_id, picking_type=picking_type or self.picking_type_id, company_id=self.company_id.id, bom_type='phantom') if bom: converted_line_quantity = current_line.product_uom_id._compute_quantity( line_quantity / bom.product_qty, bom.product_uom_id) bom_lines = [(line, current_line.product_id, converted_line_quantity, current_line) for line in bom.bom_line_ids] + bom_lines for bom_line in bom.bom_line_ids: graph[current_line.product_id.product_tmpl_id.id].append( bom_line.product_id.product_tmpl_id.id) if bom_line.product_id.product_tmpl_id.id in V and check_cycle( bom_line.product_id.product_tmpl_id.id, {key: False for key in V}, {key: False for key in V}, graph): raise UserError( _('Recursion error! A product with a Bill of Material should not have itself in its BoM or child BoMs!' )) V |= set([bom_line.product_id.product_tmpl_id.id]) boms_done.append((bom, { 'qty': converted_line_quantity, 'product': current_product, 'original_qty': quantity, 'parent_line': current_line })) else: # We round up here because the user expects that if he has to consume a little more, the whole UOM unit # should be consumed. rounding = current_line.product_uom_id.rounding line_quantity = float_round(line_quantity, precision_rounding=rounding, rounding_method='UP') lines_done.append((current_line, { 'qty': line_quantity, 'product': current_product, 'original_qty': quantity, 'parent_line': parent_line })) return boms_done, lines_done @api.model def get_import_templates(self): return [{ 'label': _('Import Template for Bills of Materials'), 'template': '/mrp/static/xls/mrp_bom.xls' }]
class BaseModuleUninstall(models.TransientModel): _name = "base.module.uninstall" _description = "Module Uninstall" show_all = fields.Boolean() module_id = fields.Many2one( 'ir.module.module', string="Module", required=True, domain=[('state', 'in', ['installed', 'to upgrade', 'to install'])], ondelete='cascade', readonly=True, ) module_ids = fields.Many2many('ir.module.module', string="Impacted modules", compute='_compute_module_ids') model_ids = fields.Many2many('ir.model', string="Impacted data models", compute='_compute_model_ids') def _get_modules(self): """ Return all the modules impacted by self. """ return self.module_id.downstream_dependencies(self.module_id) @api.depends('module_id', 'show_all') def _compute_module_ids(self): for wizard in self: modules = wizard._get_modules() wizard.module_ids = modules if wizard.show_all else modules.filtered( 'application') def _get_models(self): """ Return the models (ir.model) to consider for the impact. """ return self.env['ir.model'].search([('transient', '=', False)]) @api.depends('module_ids') def _compute_model_ids(self): ir_models = self._get_models() ir_models_xids = ir_models._get_external_ids() for wizard in self: if wizard.module_id: module_names = set(wizard._get_modules().mapped('name')) def lost(model): xids = ir_models_xids.get(model.id, ()) return xids and all( xid.split('.')[0] in module_names for xid in xids) # find the models that have all their XIDs in the given modules self.model_ids = ir_models.filtered(lost).sorted('name') @api.onchange('module_id') def _onchange_module_id(self): # if we select a technical module, show technical modules by default if not self.module_id.application: self.show_all = True @api.multi def action_uninstall(self): modules = self.mapped('module_id') return modules.button_immediate_uninstall()
class StockMove(models.Model): _inherit = 'stock.move' is_subcontract = fields.Boolean('The move is a subcontract receipt') show_subcontracting_details_visible = fields.Boolean( compute='_compute_show_subcontracting_details_visible') def _compute_show_subcontracting_details_visible(self): """ Compute if the action button in order to see moves raw is visible """ for move in self: if move.is_subcontract and move._has_tracked_subcontract_components() and\ not float_is_zero(move.quantity_done, precision_rounding=move.product_uom.rounding): move.show_subcontracting_details_visible = True else: move.show_subcontracting_details_visible = False def _compute_show_details_visible(self): """ If the move is subcontract and the components are tracked. Then the show details button is visible. """ res = super(StockMove, self)._compute_show_details_visible() for move in self: if not move.is_subcontract: continue if not move._has_tracked_subcontract_components(): continue move.show_details_visible = True return res def copy(self, default=None): self.ensure_one() if not self.is_subcontract or 'location_id' in default: return super(StockMove, self).copy(default=default) if not default: default = {} default['location_id'] = self.picking_id.location_id.id return super(StockMove, self).copy(default=default) def write(self, values): """ If the initial demand is updated then also update the linked subcontract order to the new quantity. """ if 'product_uom_qty' in values: if self.env.context.get('cancel_backorder') is False: return super(StockMove, self).write(values) self.filtered( lambda m: m.is_subcontract and m.state not in ['draft', 'cancel', 'done'])._update_subcontract_order_qty( values['product_uom_qty']) return super(StockMove, self).write(values) def action_show_details(self): """ Open the produce wizard in order to register tracked components for subcontracted product. Otherwise use standard behavior. """ self.ensure_one() if self.is_subcontract: rounding = self.product_uom.rounding production = self.move_orig_ids.production_id if self._has_tracked_subcontract_components() and\ float_compare(production.qty_produced, production.product_uom_qty, precision_rounding=rounding) < 0 and\ float_compare(self.quantity_done, self.product_uom_qty, precision_rounding=rounding) < 0: return self._action_record_components() action = super(StockMove, self).action_show_details() if self.is_subcontract: action['views'] = [ (self.env.ref('stock.view_stock_move_operations').id, 'form') ] action['context'].update({ 'show_lots_m2o': self.has_tracking != 'none', 'show_lots_text': False, }) return action def action_show_subcontract_details(self): """ Display moves raw for subcontracted product self. """ moves = self.move_orig_ids.production_id.move_raw_ids tree_view = self.env.ref( 'mrp_subcontracting.mrp_subcontracting_move_tree_view') form_view = self.env.ref( 'mrp_subcontracting.mrp_subcontracting_move_form_view') return { 'name': _('Raw Materials for %s') % (self.product_id.display_name), 'type': 'ir.actions.act_window', 'res_model': 'stock.move', 'views': [(tree_view.id, 'tree'), (form_view.id, 'form')], 'target': 'current', 'domain': [('id', 'in', moves.ids)], } def _action_confirm(self, merge=True, merge_into=False): subcontract_details_per_picking = defaultdict(list) for move in self: if move.location_id.usage != 'supplier' or move.location_dest_id.usage == 'supplier': continue if move.move_orig_ids.production_id: continue bom = move._get_subcontract_bom() if not bom: continue if float_is_zero(move.product_qty, precision_rounding=move.product_uom.rounding) and\ move.picking_id.immediate_transfer is True: raise UserError(_("To subcontract, use a planned transfer.")) subcontract_details_per_picking[move.picking_id].append( (move, bom)) move.write({ 'is_subcontract': True, 'location_id': move.picking_id.partner_id.with_context( force_company=move.company_id.id). property_stock_subcontractor.id }) for picking, subcontract_details in subcontract_details_per_picking.items( ): picking._subcontracted_produce(subcontract_details) res = super(StockMove, self)._action_confirm(merge=merge, merge_into=merge_into) if subcontract_details_per_picking: self.env['stock.picking'].concat( *list(subcontract_details_per_picking.keys())).action_assign() return res def _action_record_components(self): action = self.env.ref('mrp.act_mrp_product_produce').read()[0] action['context'] = dict( default_production_id=self.move_orig_ids.production_id.id, default_subcontract_move_id=self.id) return action def _check_overprocessed_subcontract_qty(self): """ If a subcontracted move use tracked components. Do not allow to add quantity without the produce wizard. Instead update the initial demand and use the register component button. Split or correct a lot/sn is possible. """ overprocessed_moves = self.env['stock.move'] for move in self: if not move.is_subcontract: continue # Extra quantity is allowed when components do not need to be register if not move._has_tracked_subcontract_components(): continue rounding = move.product_uom.rounding if float_compare(move.quantity_done, move.move_orig_ids.production_id.qty_produced, precision_rounding=rounding) > 0: overprocessed_moves |= move if overprocessed_moves: raise UserError( _(""" You have to use 'Records Components' button in order to register quantity for a subcontracted product(s) with tracked component(s): %s. If you want to process more than initially planned, you can use the edit + unlock buttons in order to adapt the initial demand on the operations.""") % ('\n'.join( overprocessed_moves.mapped('product_id.display_name')))) def _get_subcontract_bom(self): self.ensure_one() bom = self.env['mrp.bom'].sudo()._bom_subcontract_find( product=self.product_id, picking_type=self.picking_type_id, company_id=self.company_id.id, bom_type='subcontract', subcontractor=self.picking_id.partner_id, ) return bom def _has_tracked_subcontract_components(self): self.ensure_one() return any(m.has_tracking != 'none' for m in self.move_orig_ids.production_id.move_raw_ids) def _prepare_extra_move_vals(self, qty): vals = super(StockMove, self)._prepare_extra_move_vals(qty) vals['location_id'] = self.location_id.id return vals def _prepare_move_split_vals(self, qty): vals = super(StockMove, self)._prepare_move_split_vals(qty) vals['location_id'] = self.location_id.id return vals def _should_bypass_reservation(self): """ If the move is subcontracted then ignore the reservation. """ should_bypass_reservation = super(StockMove, self)._should_bypass_reservation() if not should_bypass_reservation and self.is_subcontract: return True return should_bypass_reservation def _update_subcontract_order_qty(self, quantity): for move in self: quantity_change = quantity - move.product_uom_qty production = move.move_orig_ids.production_id if production: self.env['change.production.qty'].with_context( skip_activity=True).create({ 'mo_id': production.id, 'product_qty': production.product_uom_qty + quantity_change }).change_prod_qty()
class View(models.Model): _name = "ir.ui.view" _inherit = ["ir.ui.view", "website.seo.metadata"] customize_show = fields.Boolean("Show As Optional Inherit", default=False) website_id = fields.Many2one('website', ondelete='cascade', string="Website") page_ids = fields.One2many('website.page', 'view_id') first_page_id = fields.Many2one('website.page', string='Website Page', help='First page linked to this view', compute='_compute_first_page_id') @api.multi def _compute_first_page_id(self): for view in self: view.first_page_id = self.env['website.page'].search( [('view_id', '=', view.id)], limit=1) @api.multi def write(self, vals): '''COW for ir.ui.view. This way editing websites does not impact other websites. Also this way newly created websites will only contain the default views. ''' current_website_id = self.env.context.get('website_id') if not current_website_id or self.env.context.get('no_cow'): return super(View, self).write(vals) # We need to consider inactive views when handling multi-website cow # feature (to copy inactive children views, to search for specific # views, ...) for view in self.with_context(active_test=False): # Make sure views which are written in a website context receive # a value for their 'key' field if not view.key and not vals.get('key'): view.with_context( no_cow=True).key = 'website.key_%s' % str(uuid.uuid4())[:6] # No need of COW if the view is already specific if view.website_id: super(View, view).write(vals) continue # If already a specific view for this generic view, write on it website_specific_view = view.search( [('key', '=', view.key), ('website_id', '=', current_website_id)], limit=1) if website_specific_view: super(View, website_specific_view).write(vals) continue # Set key to avoid copy() to generate an unique key as we want the # specific view to have the same key copy_vals = {'website_id': current_website_id, 'key': view.key} # Copy with the 'inherit_id' field value that will be written to # ensure the copied view's validation works if vals.get('inherit_id'): copy_vals['inherit_id'] = vals['inherit_id'] website_specific_view = view.copy(copy_vals) view._create_website_specific_pages_for_view( website_specific_view, view.env['website'].browse(current_website_id)) for inherit_child in view.inherit_children_ids.filter_duplicate( ).sorted(key=lambda v: (v.priority, v.id)): if inherit_child.website_id.id == current_website_id: # In the case the child was already specific to the current # website, we cannot just reattach it to the new specific # parent: we have to copy it there and remove it from the # original tree. Indeed, the order of children 'id' fields # must remain the same so that the inheritance is applied # in the same order in the copied tree. child = inherit_child.copy({ 'inherit_id': website_specific_view.id, 'key': inherit_child.key }) inherit_child.inherit_children_ids.write( {'inherit_id': child.id}) inherit_child.unlink() else: # Trigger COW on inheriting views inherit_child.write( {'inherit_id': website_specific_view.id}) super(View, website_specific_view).write(vals) return True @api.multi def _get_specific_views(self): """ Given a view, return a record set containing all the specific views for that view's key. If the given view is already specific, it will also return itself. """ self.ensure_one() domain = [('key', '=', self.key), ('website_id', '!=', False)] return self.with_context(active_test=False).search(domain) def _load_records_write(self, values): """ During module update, when updating a generic view, we should also update its specific views (COW'd). Note that we will only update unmodified fields. That will mimic the noupdate behavior on views having an ir.model.data. """ if self.type == 'qweb' and not self.website_id: # Update also specific views for cow_view in self._get_specific_views(): authorized_vals = {} for key in values: if cow_view[key] == self[key]: authorized_vals[key] = values[key] cow_view.write(authorized_vals) super(View, self)._load_records_write(values) def _load_records_create(self, values): """ During module install, when creating a generic child view, we should also create that view under specific view trees (COW'd). Top level view (no inherit_id) do not need that behavior as they will be shared between websites since there is no specific yet. """ records = super(View, self)._load_records_create(values) for record in records: if record.type == 'qweb' and record.inherit_id and not record.website_id and not record.inherit_id.website_id: specific_parent_views = record.with_context( active_test=False).search([ ('key', '=', record.inherit_id.key), ('website_id', '!=', None), ]) for specific_parent_view in specific_parent_views: record.with_context( website_id=specific_parent_view.website_id.id).write({ 'inherit_id': specific_parent_view.id, }) return records @api.multi def unlink(self): '''This implements COU (copy-on-unlink). When deleting a generic page website-specific pages will be created so only the current website is affected. ''' current_website_id = self._context.get('website_id') if current_website_id and not self._context.get('no_cow'): for view in self.filtered(lambda view: not view.website_id): for website in self.env['website'].search([ ('id', '!=', current_website_id) ]): # reuse the COW mechanism to create # website-specific copies, it will take # care of creating pages and menus. view.with_context(website_id=website.id).write( {'name': view.name}) specific_views = self.env['ir.ui.view'] if self and self.pool._init: for view in self: specific_views += view._get_specific_views() result = super(View, self + specific_views).unlink() self.clear_caches() return result def _create_website_specific_pages_for_view(self, new_view, website): for page in self.page_ids: # create new pages for this view page.copy({ 'view_id': new_view.id, 'is_published': page.is_published, }) @api.model def get_related_views(self, key, bundles=False): '''Make this only return most specific views for website.''' # get_related_views can be called through website=False routes # (e.g. /web_editor/get_assets_editor_resources), so website # dispatch_parameters may not be added. Manually set # website_id. (It will then always fallback on a website, this # method should never be called in a generic context, even for # tests) self = self.with_context( website_id=self.env['website'].get_current_website().id) return super(View, self).get_related_views(key, bundles=bundles) def filter_duplicate(self): """ Filter current recordset only keeping the most suitable view per distinct key. Every non-accessible view will be removed from the set: * In non website context, every view with a website will be removed * In a website context, every view from another website """ current_website_id = self._context.get('website_id') most_specific_views = self.env['ir.ui.view'] if not current_website_id: return self.filtered(lambda view: not view.website_id) for view in self: # specific view: add it if it's for the current website and ignore # it if it's for another website if view.website_id and view.website_id.id == current_website_id: most_specific_views |= view # generic view: add it only if, for the current website, there is no # specific view for this view (based on the same `key` attribute) elif not view.website_id and not any( view.key == view2.key and view2.website_id and view2.website_id.id == current_website_id for view2 in self): most_specific_views |= view return most_specific_views @api.model def _view_get_inherited_children(self, view, options): extensions = super(View, self)._view_get_inherited_children(view, options) return extensions.filter_duplicate() @api.model def _view_obj(self, view_id): ''' Given an xml_id or a view_id, return the corresponding view record. In case of website context, return the most specific one. :param view_id: either a string xml_id or an integer view_id :return: The view record or empty recordset ''' if isinstance(view_id, pycompat.string_types) or isinstance( view_id, pycompat.integer_types): return self.env['website'].viewref(view_id) else: # It can already be a view object when called by '_views_get()' that is calling '_view_obj' # for it's inherit_children_ids, passing them directly as object record. (Note that it might # be a view_id from another website but it will be filtered in 'get_related_views()') return view_id if view_id._name == 'ir.ui.view' else self.env[ 'ir.ui.view'] @api.model def _get_inheriting_views_arch_website(self, view_id): return self.env['website'].browse(self._context.get('website_id')) @api.model def _get_inheriting_views_arch_domain(self, view_id, model): domain = super(View, self)._get_inheriting_views_arch_domain(view_id, model) current_website = self._get_inheriting_views_arch_website(view_id) website_views_domain = current_website.website_domain() # when rendering for the website we have to include inactive views # we will prefer inactive website-specific views over active generic ones if current_website: domain = [leaf for leaf in domain if 'active' not in leaf] return expression.AND([website_views_domain, domain]) @api.model def get_inheriting_views_arch(self, view_id, model): if not self._context.get('website_id'): return super(View, self).get_inheriting_views_arch(view_id, model) inheriting_views = super(View, self.with_context( active_test=False)).get_inheriting_views_arch(view_id, model) # prefer inactive website-specific views over active generic ones inheriting_views = self.browse([ view[1] for view in inheriting_views ]).filter_duplicate().filtered('active') return [(view.arch, view.id) for view in inheriting_views] @api.model @tools.ormcache_context('self._uid', 'xml_id', keys=('website_id', )) def get_view_id(self, xml_id): """If a website_id is in the context and the given xml_id is not an int then try to get the id of the specific view for that website, but fallback to the id of the generic view if there is no specific. If no website_id is in the context, it might randomly return the generic or the specific view, so it's probably not recommanded to use this method. `viewref` is probably more suitable. Archived views are ignored (unless the active_test context is set, but then the ormcache_context will not work as expected). """ if 'website_id' in self._context and not isinstance( xml_id, pycompat.integer_types): current_website = self.env['website'].browse( self._context.get('website_id')) domain = ['&', ('key', '=', xml_id)] + current_website.website_domain() view = self.search(domain, order='website_id', limit=1) if not view: _logger.warning("Could not find view object with xml_id '%s'", xml_id) raise ValueError('View %r in website %r not found' % (xml_id, self._context['website_id'])) return view.id return super(View, self).get_view_id(xml_id) @api.multi def _get_original_view(self): """Given a view, retrieve the original view it was COW'd from. The given view might already be the original one. In that case it will (and should) return itself. """ self.ensure_one() domain = [('key', '=', self.key), ('model_data_id', '!=', None)] return self.with_context(active_test=False).search( domain, limit=1) # Useless limit has multiple xmlid should not be possible @api.multi def render(self, values=None, engine='ir.qweb', minimal_qcontext=False): """ Render the template. If website is enabled on request, then extend rendering context with website values. """ new_context = dict(self._context) if request and getattr(request, 'is_frontend', False): editable = request.website.is_publisher() translatable = editable and self._context.get( 'lang') != request.website.default_lang_code editable = not translatable and editable # in edit mode ir.ui.view will tag nodes if not translatable and not self.env.context.get( 'rendering_bundle'): if editable: new_context = dict(self._context, inherit_branding=True) elif request.env.user.has_group( 'website.group_website_publisher'): new_context = dict(self._context, inherit_branding_auto=True) # Fallback incase main_object dont't inherit 'website.seo.metadata' if values and 'main_object' in values and not hasattr( values['main_object'], 'get_website_meta'): values['main_object'].get_website_meta = lambda: {} if self._context != new_context: self = self.with_context(new_context) return super(View, self).render(values, engine=engine, minimal_qcontext=minimal_qcontext) @api.model def _prepare_qcontext(self): """ Returns the qcontext : rendering context with website specific value (required to render website layout template) """ qcontext = super(View, self)._prepare_qcontext() if request and getattr(request, 'is_frontend', False): Website = self.env['website'] editable = request.website.is_publisher() translatable = editable and self._context.get( 'lang') != request.env['ir.http']._get_default_lang().code editable = not translatable and editable if 'main_object' not in qcontext: qcontext['main_object'] = self cur = Website.get_current_website() qcontext['multi_website_websites_current'] = { 'website_id': cur.id, 'name': cur.name, 'domain': cur.domain } qcontext['multi_website_websites'] = [{ 'website_id': website.id, 'name': website.name, 'domain': website.domain } for website in Website.search([]) if website != cur] cur_company = self.env.user.company_id qcontext['multi_website_companies_current'] = { 'company_id': cur_company.id, 'name': cur_company.name } qcontext['multi_website_companies'] = [{ 'company_id': comp.id, 'name': comp.name } for comp in self.env.user.company_ids if comp != cur_company] qcontext.update( dict( self._context.copy(), website=request.website, url_for=url_for, res_company=request.website.company_id.sudo(), default_lang_code=request.env['ir.http']._get_default_lang( ).code, languages=request.env['ir.http']._get_language_codes(), translatable=translatable, editable=editable, menu_data=self.env['ir.ui.menu'].load_menus_root() if request.website.is_user() else None, )) return qcontext @api.model def get_default_lang_code(self): website_id = self.env.context.get('website_id') if website_id: lang_code = self.env['website'].browse( website_id).default_lang_code return lang_code else: return super(View, self).get_default_lang_code() @api.multi def redirect_to_page_manager(self): return { 'type': 'ir.actions.act_url', 'url': '/website/pages', 'target': 'self', } def _read_template_keys(self): return super(View, self)._read_template_keys() + ['website_id'] @api.model def _save_oe_structure_hook(self): res = super(View, self)._save_oe_structure_hook() res['website_id'] = self.env['website'].get_current_website().id return res @api.model def _set_noupdate(self): '''If website is installed, any call to `save` from the frontend will actually write on the specific view (or create it if not exist yet). In that case, we don't want to flag the generic view as noupdate. ''' if not self._context.get('website_id'): super(View, self)._set_noupdate() @api.multi def save(self, value, xpath=None): self.ensure_one() current_website = self.env['website'].get_current_website() # xpath condition is important to be sure we are editing a view and not # a field as in that case `self` might not exist (check commit message) if xpath and self.key and current_website: # The first time a generic view is edited, if multiple editable parts # were edited at the same time, multiple call to this method will be # done but the first one may create a website specific view. So if there # already is a website specific view, we need to divert the super to it. website_specific_view = self.env['ir.ui.view'].search( [('key', '=', self.key), ('website_id', '=', current_website.id)], limit=1) if website_specific_view: self = website_specific_view super(View, self).save(value, xpath=xpath)