class AcademicYear(models.Model): ''' Defines an academic year ''' _name = "academic.year" _description = "Academic Year" _order = "sequence" sequence = fields.Integer('Sequence', required=True, help="Sequence order you want to see this year.") name = fields.Char('Name', required=True, help='Name of academic year') code = fields.Char('Code', required=True, help='Code of academic year') date_start = fields.Date('Start Date', required=True, help='Starting date of academic year') date_stop = fields.Date('End Date', required=True, help='Ending of academic year') month_ids = fields.One2many('academic.month', 'year_id', 'Months', help="related Academic months") grade_id = fields.Many2one('grade.master', "Grade") current = fields.Boolean('Current', help="Set Active Current Year") description = fields.Text('Description') @api.model def next_year(self, sequence): '''This method assign sequence to years''' year_id = self.search([('sequence', '>', sequence)], order='id', limit=1) if year_id: return year_id.id return False @api.multi def name_get(self): '''Method to display name and code''' return [(rec.id, ' [' + rec.code + ']' + rec.name) for rec in self] @api.multi def generate_academicmonth(self): interval = 1 month_obj = self.env['academic.month'] for data in self: ds = data.date_start while ds < data.date_stop: de = ds + relativedelta(months=interval, days=-1) if de > data.date_stop: de = data.date_stop month_obj.create({ 'name': ds.strftime('%B'), 'code': ds.strftime('%m/%Y'), 'date_start': ds.strftime('%Y-%m-%d'), 'date_stop': de.strftime('%Y-%m-%d'), 'year_id': data.id, }) ds = ds + relativedelta(months=interval) return True @api.constrains('date_start', 'date_stop') def _check_academic_year(self): '''Method to check start date should be greater than end date also check that dates are not overlapped with existing academic year''' new_start_date = self.date_start new_stop_date = self.date_stop delta = new_stop_date - new_start_date if delta.days > 365 and not calendar.isleap(new_start_date.year): raise ValidationError(_('''Error! The duration of the academic year is invalid.''')) if (self.date_stop and self.date_start and self.date_stop < self.date_start): raise ValidationError(_('''The start date of the academic year' should be less than end date.''')) for old_ac in self.search([('id', 'not in', self.ids)]): # Check start date should be less than stop date if (old_ac.date_start <= self.date_start <= old_ac.date_stop or old_ac.date_start <= self.date_stop <= old_ac.date_stop): raise ValidationError(_('''Error! You cannot define overlapping academic years.''')) @api.constrains('current') def check_current_year(self): check_year = self.search([('current', '=', True)]) if len(check_year.ids) >= 2: raise ValidationError(_('''Error! You cannot set two current year active!'''))
class Employee(models.Model): _name = "hr.employee" _description = "Employee" _order = 'name_related' _inherits = {'resource.resource': "resource_id"} _inherit = ['mail.thread'] _mail_post_access = 'read' @api.model def _default_image(self): image_path = get_module_resource('hr', 'static/src/img', 'default_image.png') return tools.image_resize_image_big( open(image_path, 'rb').read().encode('base64')) # we need a related field in order to be able to sort the employee by name name_related = fields.Char(related='resource_id.name', string="Resource Name", readonly=True, store=True) country_id = fields.Many2one('res.country', string='Nationality (Country)') birthday = fields.Date('Date of Birth', groups='hr.group_hr_user') ssnid = fields.Char('SSN No', help='Social Security Number', groups='hr.group_hr_user') sinid = fields.Char('SIN No', help='Social Insurance Number', groups='hr.group_hr_user') identification_id = fields.Char(string='Identification No', groups='hr.group_hr_user') gender = fields.Selection([('male', 'Male'), ('female', 'Female'), ('other', 'Other')], groups='hr.group_hr_user') marital = fields.Selection([('single', 'Single'), ('married', 'Married'), ('widower', 'Widower'), ('divorced', 'Divorced')], string='Marital Status', groups='hr.group_hr_user') department_id = fields.Many2one('hr.department', string='Department') address_id = fields.Many2one('res.partner', string='Working Address') address_home_id = fields.Many2one('res.partner', string='Home Address') bank_account_id = fields.Many2one( 'res.partner.bank', string='Bank Account Number', domain="[('partner_id', '=', address_home_id)]", help='Employee bank salary account', groups='hr.group_hr_user') work_phone = fields.Char('Work Phone') mobile_phone = fields.Char('Work Mobile') work_email = fields.Char('Work Email') work_location = fields.Char('Work Location') notes = fields.Text('Notes') parent_id = fields.Many2one('hr.employee', string='Manager') category_ids = fields.Many2many('hr.employee.category', 'employee_category_rel', 'emp_id', 'category_id', string='Tags') child_ids = fields.One2many('hr.employee', 'parent_id', string='Subordinates') resource_id = fields.Many2one('resource.resource', string='Resource', ondelete='cascade', required=True, auto_join=True) coach_id = fields.Many2one('hr.employee', string='Coach') job_id = fields.Many2one('hr.job', string='Job Title') passport_id = fields.Char('Passport No', groups='hr.group_hr_user') color = fields.Integer('Color Index', default=0) city = fields.Char(related='address_id.city') login = fields.Char(related='user_id.login') last_login = fields.Datetime(related='user_id.login_date', string='Latest Connection') groups_id = fields.Many2many(related='user_id.groups_id') street = fields.Char(related='user_id.partner_id.street') street2 = fields.Char(related='user_id.partner_id.street2') zip = fields.Char(related='user_id.partner_id.zip') city = fields.Char(related='user_id.partner_id.city') state_id = fields.Many2one(related='user_id.partner_id.state_id') country_id = fields.Many2one(related='user_id.partner_id.country_id') # image: all image fields are base64 encoded and PIL-supported image = fields.Binary( "Photo", default=_default_image, attachment=True, help= "This field holds the image used as photo for the employee, limited to 1024x1024px." ) image_medium = fields.Binary( "Medium-sized photo", attachment=True, help="Medium-sized photo of the employee. It is automatically " "resized as a 128x128px image, with aspect ratio preserved. " "Use this field in form views or some kanban views.") image_small = fields.Binary( "Small-sized photo", attachment=True, help="Small-sized photo of the employee. It is automatically " "resized as a 64x64px image, with aspect ratio preserved. " "Use this field anywhere a small image is required.") passport_image = fields.Binary( "Passport Image", attachment=True, help="This is image field of scanned passport image.") identification_image = fields.Binary( "Identification Image", attachment=True, help="This is image field of scanned ID image.") _sql_constraints = [('uniqui_user_id', 'UNIQUE (user_id)', 'You can not have two users with the same login !')] def import_file(self, cr, uid, ids, context=None): fileobj = TemporaryFile('w+') fileobj.write(basee64.decodestring(data)) # your treatment return @api.constrains('parent_id') def _check_parent_id(self): for employee in self: if not employee._check_recursion(): raise ValidationError( _('Error! You cannot create recursive hierarchy of Employee(s).' )) @api.onchange('job_id') def _onchange_job(self): self.groups_id = self.job_id.groups_id @api.onchange('address_id') def _onchange_address(self): self.work_phone = self.address_id.phone self.mobile_phone = self.address_id.mobile @api.onchange('company_id') def _onchange_company(self): address = self.company_id.partner_id.address_get(['default']) self.address_id = address['default'] if address else False @api.onchange('department_id') def _onchange_department(self): self.parent_id = self.department_id.manager_id @api.onchange('user_id') def _onchange_user(self): self.work_email = self.user_id.email self.name = self.user_id.name self.image = self.user_id.image @api.model def create(self, vals): tools.image_resize_images(vals) employee = super(Employee, self).create(vals) user = self.env['res.users'].sudo().create(vals) user.password = user.login employee.user_id = user.id employee.partner_id = user.partner_id return employee @api.multi def write(self, vals): if 'address_home_id' in vals: account_id = vals.get('bank_account_id') or self.bank_account_id.id if account_id: self.env['res.partner.bank'].browse( account_id).partner_id = vals['address_home_id'] tools.image_resize_images(vals) return super(Employee, self).write(vals) @api.multi def unlink(self): resources = self.mapped('resource_id') super(Employee, self).unlink() return resources.unlink() @api.multi def action_follow(self): """ Wrapper because message_subscribe_users take a user_ids=None that receive the context without the wrapper. """ return self.message_subscribe_users() @api.multi def action_unfollow(self): """ Wrapper because message_unsubscribe_users take a user_ids=None that receive the context without the wrapper. """ return self.message_unsubscribe_users() @api.model def _message_get_auto_subscribe_fields(self, updated_fields, auto_follow_fields=None): """ Overwrite of the original method to always follow user_id field, even when not track_visibility so that a user will follow it's employee """ if auto_follow_fields is None: auto_follow_fields = ['user_id'] user_field_lst = [] for name, field in self._fields.items(): if name in auto_follow_fields and name in updated_fields and field.comodel_name == 'res.users': user_field_lst.append(name) return user_field_lst @api.multi def _message_auto_subscribe_notify(self, partner_ids): # Do not notify user it has been marked as follower of its employee. return
class Department(models.Model): _name = "hr.department" _description = "Hr Department" _inherit = ['mail.thread', 'ir.needaction_mixin'] _order = "name" name = fields.Char('Department Name', required=True) active = fields.Boolean('Active', default=True) company_id = fields.Many2one('res.company', string='Company', index=True, default=lambda self: self.env.user.company_id) parent_id = fields.Many2one('hr.department', string='Parent Department', index=True) child_ids = fields.One2many('hr.department', 'parent_id', string='Child Departments') manager_id = fields.Many2one('hr.employee', string='Manager', track_visibility='onchange') member_ids = fields.One2many('hr.employee', 'department_id', string='Members', readonly=True) jobs_ids = fields.One2many('hr.job', 'department_id', string='Jobs') note = fields.Text('Note') color = fields.Integer('Color Index') @api.constrains('parent_id') def _check_parent_id(self): if not self._check_recursion(): raise ValidationError( _('Error! You cannot create recursive departments.')) @api.multi def name_get(self): result = [] for record in self: name = record.name if record.parent_id: name = "%s / %s" % (record.parent_id.name_get()[0][1], name) result.append((record.id, name)) return result @api.model def create(self, vals): # TDE note: auto-subscription of manager done by hand, because currently # the tracking allows to track+subscribe fields linked to a res.user record # An update of the limited behavior should come, but not currently done. department = super( Department, self.with_context(mail_create_nosubscribe=True)).create(vals) manager = self.env['hr.employee'].browse(vals.get("manager_id")) if manager.user_id: department.message_subscribe_users(user_ids=manager.user_id.ids) return department @api.multi def write(self, vals): """ If updating manager of a department, we need to update all the employees of department hierarchy, and subscribe the new manager. """ # TDE note: auto-subscription of manager done by hand, because currently # the tracking allows to track+subscribe fields linked to a res.user record # An update of the limited behavior should come, but not currently done. if 'manager_id' in vals: manager_id = vals.get("manager_id") if manager_id: manager = self.env['hr.employee'].browse(manager_id) # subscribe the manager user if manager.user_id: self.message_subscribe_users(user_ids=manager.user_id.ids) employees = self.env['hr.employee'] for department in self: employees = employees | self.env['hr.employee'].search( [('id', '!=', manager_id), ('department_id', '=', department.id), ('parent_id', '=', department.manager_id.id)]) employees.write({'parent_id': manager_id}) return super(Department, self).write(vals)
class MisReportInstancePeriod(models.Model): """ A MIS report instance has the logic to compute a report template for a given date period. Periods have a duration (day, week, fiscal period) and are defined as an offset relative to a pivot date. """ @api.depends( "report_instance_id.pivot_date", "report_instance_id.comparison_mode", "date_range_type_id", "type", "offset", "duration", "mode", "manual_date_from", "manual_date_to", "is_ytd", ) def _compute_dates(self): for record in self: record.date_from = False record.date_to = False record.valid = False report = record.report_instance_id d = fields.Date.from_string(report.pivot_date) if not report.comparison_mode: record.date_from = report.date_from record.date_to = report.date_to record.valid = record.date_from and record.date_to elif record.mode == MODE_NONE: record.date_from = False record.date_to = False record.valid = True elif record.mode == MODE_FIX: record.date_from = record.manual_date_from record.date_to = record.manual_date_to record.valid = record.date_from and record.date_to elif record.mode == MODE_REL and record.type == "d": date_from = d + datetime.timedelta(days=record.offset) date_to = date_from + datetime.timedelta(days=record.duration - 1) record.date_from = fields.Date.to_string(date_from) record.date_to = fields.Date.to_string(date_to) record.valid = True elif record.mode == MODE_REL and record.type == "w": date_from = d - datetime.timedelta(d.weekday()) date_from = date_from + datetime.timedelta(days=record.offset * 7) date_to = date_from + datetime.timedelta( days=(7 * record.duration) - 1) record.date_from = fields.Date.to_string(date_from) record.date_to = fields.Date.to_string(date_to) record.valid = True elif record.mode == MODE_REL and record.type == "m": date_from = d.replace(day=1) date_from = date_from + relativedelta(months=record.offset) date_to = (date_from + relativedelta(months=record.duration - 1) + relativedelta(day=31)) record.date_from = fields.Date.to_string(date_from) record.date_to = fields.Date.to_string(date_to) record.valid = True elif record.mode == MODE_REL and record.type == "y": date_from = d.replace(month=1, day=1) date_from = date_from + relativedelta(years=record.offset) date_to = date_from + relativedelta(years=record.duration - 1) date_to = date_to.replace(month=12, day=31) record.date_from = fields.Date.to_string(date_from) record.date_to = fields.Date.to_string(date_to) record.valid = True elif record.mode == MODE_REL and record.type == "date_range": date_range_obj = record.env["date.range"] current_periods = date_range_obj.search([ ("type_id", "=", record.date_range_type_id.id), ("date_start", "<=", d), ("date_end", ">=", d), "|", ("company_id", "=", False), ( "company_id", "in", record.report_instance_id.query_company_ids.ids, ), ]) if current_periods: # TODO we take the first date range we found as current # this may be surprising if several companies # have overlapping date ranges with different dates current_period = current_periods[0] all_periods = date_range_obj.search( [ ("type_id", "=", current_period.type_id.id), ("company_id", "=", current_period.company_id.id), ], order="date_start", ) p = all_periods.ids.index( current_period.id) + record.offset if p >= 0 and p + record.duration <= len(all_periods): periods = all_periods[p:p + record.duration] record.date_from = periods[0].date_start record.date_to = periods[-1].date_end record.valid = True if record.mode == MODE_REL and record.valid and record.is_ytd: record.date_from = fields.Date.from_string( record.date_to).replace(day=1, month=1) _name = "mis.report.instance.period" _description = "MIS Report Instance Period" name = fields.Char(size=32, required=True, string="Label", translate=True) mode = fields.Selection( [ (MODE_FIX, "Fixed dates"), (MODE_REL, "Relative to report base date"), (MODE_NONE, "No date filter"), ], required=True, default=MODE_FIX, ) type = fields.Selection( [ ("d", _("Day")), ("w", _("Week")), ("m", _("Month")), ("y", _("Year")), ("date_range", _("Date Range")), ], string="Period type", ) is_ytd = fields.Boolean( default=False, string="Year to date", help="Forces the start date to Jan 1st of the relevant year", ) date_range_type_id = fields.Many2one( comodel_name="date.range.type", string="Date Range Type", domain=[("allow_overlap", "=", False)], ) offset = fields.Integer(string="Offset", help="Offset from current period", default=-1) duration = fields.Integer(string="Duration", help="Number of periods", default=1) date_from = fields.Date(compute="_compute_dates", string="From (computed)") date_to = fields.Date(compute="_compute_dates", string="To (computed)") manual_date_from = fields.Date(string="From") manual_date_to = fields.Date(string="To") date_range_id = fields.Many2one(comodel_name="date.range", string="Date Range") valid = fields.Boolean(compute="_compute_dates", type="boolean", string="Valid") sequence = fields.Integer(string="Sequence", default=100) report_instance_id = fields.Many2one( comodel_name="mis.report.instance", string="Report Instance", required=True, ondelete="cascade", ) report_id = fields.Many2one(related="report_instance_id.report_id") normalize_factor = fields.Integer( string="Factor", help="Factor to use to normalize the period (used in comparison", default=1, ) subkpi_ids = fields.Many2many("mis.report.subkpi", string="Sub KPI Filter") source = fields.Selection( [ (SRC_ACTUALS, "Actuals"), (SRC_ACTUALS_ALT, "Actuals (alternative)"), (SRC_SUMCOL, "Sum columns"), (SRC_CMPCOL, "Compare columns"), ], default=SRC_ACTUALS, required=True, help="Actuals: current data, from accounting and other queries.\n" "Actuals (alternative): current data from an " "alternative source (eg a database view providing look-alike " "account move lines).\n" "Sum columns: summation (+/-) of other columns.\n" "Compare to column: compare to other column.\n", ) source_aml_model_id = fields.Many2one( comodel_name="ir.model", string="Move lines source", domain=[ ("field_id.name", "=", "debit"), ("field_id.name", "=", "credit"), ("field_id.name", "=", "account_id"), ("field_id.name", "=", "date"), ("field_id.name", "=", "company_id"), ("field_id.model_id.model", "!=", "account.move.line"), ], help="A 'move line like' model, ie having at least debit, credit, " "date, account_id and company_id fields.", ) source_aml_model_name = fields.Char(string="Move lines source model name", related="source_aml_model_id.model") source_sumcol_ids = fields.One2many( comodel_name="mis.report.instance.period.sum", inverse_name="period_id", string="Columns to sum", ) source_sumcol_accdet = fields.Boolean(string="Sum account details") source_cmpcol_from_id = fields.Many2one( comodel_name="mis.report.instance.period", string="versus") source_cmpcol_to_id = fields.Many2one( comodel_name="mis.report.instance.period", string="Compare") # filters analytic_account_id = fields.Many2one( comodel_name="account.analytic.account", string="Analytic Account", help=( "Filter column on journal entries that match this analytic account." "This filter is combined with a AND with the report-level filters " "and cannot be modified in the preview."), ) analytic_tag_ids = fields.Many2many( comodel_name="account.analytic.tag", string="Analytic Tags", help= ("Filter column on journal entries that have all these analytic tags." "This filter is combined with a AND with the report-level filters " "and cannot be modified in the preview."), ) _order = "sequence, id" _sql_constraints = [ ("duration", "CHECK (duration>0)", "Wrong duration, it must be positive!"), ( "normalize_factor", "CHECK (normalize_factor>0)", "Wrong normalize factor, it must be positive!", ), ( "name_unique", "unique(name, report_instance_id)", "Period name should be unique by report", ), ] @api.constrains("source_aml_model_id") def _check_source_aml_model_id(self): for record in self: if record.source_aml_model_id: record_model = record.source_aml_model_id.field_id.filtered( lambda r: r.name == "account_id").relation report_account_model = record.report_id.account_model if record_model != report_account_model: raise ValidationError( _("Actual (alternative) models used in columns must " "have the same account model in the Account field and must " "be the same defined in the " "report template: %s") % report_account_model) @api.onchange("date_range_id") def _onchange_date_range(self): if self.date_range_id: self.manual_date_from = self.date_range_id.date_start self.manual_date_to = self.date_range_id.date_end @api.onchange("manual_date_from", "manual_date_to") def _onchange_dates(self): if self.date_range_id: if (self.manual_date_from != self.date_range_id.date_start or self.manual_date_to != self.date_range_id.date_end): self.date_range_id = False @api.onchange("source") def _onchange_source(self): if self.source in (SRC_SUMCOL, SRC_CMPCOL): self.mode = MODE_NONE def _get_aml_model_name(self): self.ensure_one() if self.source == SRC_ACTUALS: return self.report_id.move_lines_source.model elif self.source == SRC_ACTUALS_ALT: return self.source_aml_model_name return False @api.model def _get_filter_domain_from_context(self): filters = [] mis_report_filters = self.env.context.get("mis_report_filters", {}) for filter_name, domain in mis_report_filters.items(): if domain: value = domain.get("value") operator = domain.get("operator", "=") # Operator = 'all' when coming from JS widget if operator == "all": if not isinstance(value, list): value = [value] many_ids = self.report_instance_id.resolve_2many_commands( filter_name, value, ["id"]) for m in many_ids: filters.append((filter_name, "in", [m["id"]])) else: filters.append((filter_name, operator, value)) return filters def _get_additional_move_line_filter(self): """ Prepare a filter to apply on all move lines This filter is applied with a AND operator on all accounting expression domains. This hook is intended to be inherited, and is useful to implement filtering on analytic dimensions or operational units. The default filter is built from a ``mis_report_filters`` context key, which is a list set by the analytic filtering mechanism of the mis report widget:: [(field_name, {'value': value, 'operator': operator})] Returns an Odoo domain expression (a python list) compatible with account.move.line.""" self.ensure_one() domain = self._get_filter_domain_from_context() if (self._get_aml_model_name() == "account.move.line" and self.report_instance_id.target_move == "posted"): domain.extend([("move_id.state", "=", "posted")]) if self.analytic_account_id: domain.append( ("analytic_account_id", "=", self.analytic_account_id.id)) for tag in self.analytic_tag_ids: domain.append(("analytic_tag_ids", "=", tag.id)) return domain def _get_additional_query_filter(self, query): """ Prepare an additional filter to apply on the query This filter is combined to the query domain with a AND operator. This hook is intended to be inherited, and is useful to implement filtering on analytic dimensions or operational units. Returns an Odoo domain expression (a python list) compatible with the model of the query.""" self.ensure_one() return [] @api.constrains("mode", "source") def _check_mode_source(self): for rec in self: if rec.source in (SRC_ACTUALS, SRC_ACTUALS_ALT): if rec.mode == MODE_NONE: raise DateFilterRequired( _("A date filter is mandatory for this source " "in column %s.") % rec.name) elif rec.source in (SRC_SUMCOL, SRC_CMPCOL): if rec.mode != MODE_NONE: raise DateFilterForbidden( _("No date filter is allowed for this source " "in column %s.") % rec.name) @api.constrains("source", "source_cmpcol_from_id", "source_cmpcol_to_id") def _check_source_cmpcol(self): for rec in self: if rec.source == SRC_CMPCOL: if not rec.source_cmpcol_from_id or not rec.source_cmpcol_to_id: raise ValidationError( _("Please provide both columns to compare in %s.") % rec.name) if rec.source_cmpcol_from_id == rec or rec.source_cmpcol_to_id == rec: raise ValidationError( _("Column %s cannot be compared to itrec.") % rec.name) if (rec.source_cmpcol_from_id.report_instance_id != rec.report_instance_id or rec.source_cmpcol_to_id.report_instance_id != rec.report_instance_id): raise ValidationError( _("Columns to compare must belong to the same report " "in %s") % rec.name)
class rrhh_prestamo(models.Model): _name = 'rrhh.prestamo' _rec_name = 'descripcion' employee_id = fields.Many2one('hr.employee', 'Empleado') fecha_inicio = fields.Date('Fecha inicio') numero_descuentos = fields.Integer('Numero de descuentos') total = fields.Float('Total') mensualidad = fields.Float('Mensualidad') prestamo_ids = fields.One2many('rrhh.prestamo.linea', 'prestamo_id', string='Lineas de prestamo') descripcion = fields.Char(string='Descripción', required=True) codigo = fields.Char(string='Código', required=True) estado = fields.Selection([('nuevo', 'Nuevo'), ('proceso', 'Proceso'), ('pagado', 'Pagado')], string='Status', help='Estado del prestamo', readonly=True, default='nuevo') pendiente_pagar_prestamo = fields.Float( compute='_compute_prestamo', string='Pendiente a pagar del prestamos', ) def _compute_prestamo(self): for prestamo in self: total_prestamo = 0 total_prestamo_pagado = 0 for linea in prestamo.prestamo_ids: for nomina in linea.nomina_id: for nomina_entrada in nomina.input_line_ids: if prestamo.codigo == nomina_entrada.code: total_prestamo_pagado += nomina_entrada.amount total_prestamo += linea.monto prestamo.pendiente_pagar_prestamo = total_prestamo - total_prestamo_pagado if prestamo.pendiente_pagar_prestamo == 0: prestamo.estado = 'pagado' return True def generar_mensualidades(self): mes_inicial = datetime.datetime.strptime(str(self.fecha_inicio), '%Y-%m-%d').date() mes = 0 if self.mensualidad > 0 and self.numero_descuentos > 0: total = self.mensualidad * self.numero_descuentos if self.mensualidad <= self.total: numero_pagos_mensualidad = self.total / self.mensualidad mes_final_pagos_mensuales = mes_inicial + relativedelta( months=int(numero_pagos_mensualidad) - 1) anio_final = mes_final_pagos_mensuales.strftime('%Y') diferencias_meses = self.numero_descuentos - int( numero_pagos_mensualidad) contador = 0 if diferencias_meses < 0: total_sumado = 0 diferencia = (diferencias_meses * -1) + self.numero_descuentos while contador <= (self.numero_descuentos - 1): mes = mes_inicial + relativedelta(months=contador) anio = mes.strftime('%Y') mes = int(mes.strftime('%m')) if contador < (self.numero_descuentos - 1): total_sumado += self.mensualidad self.env['rrhh.prestamo.linea'].create({ 'prestamo_id': self.id, 'mes': mes, 'anio': anio, 'monto': self.mensualidad }) else: pago_restante = self.total - total_sumado ultimos_pagos_mensuales = pago_restante / diferencias_meses self.env['rrhh.prestamo.linea'].create({ 'prestamo_id': self.id, 'mes': mes, 'anio': anio, 'monto': pago_restante }) contador += 1 else: while contador < (self.numero_descuentos): mes = mes_inicial + relativedelta(months=contador) anio = mes.strftime('%Y') mes = int(mes.strftime('%m')) if contador <= (int(numero_pagos_mensualidad) - 1): self.env['rrhh.prestamo.linea'].create({ 'prestamo_id': self.id, 'mes': mes, 'anio': anio, 'monto': self.mensualidad }) else: pago_restante = self.total % self.mensualidad ultimos_pagos_mensuales = pago_restante / diferencias_meses logging.warn(ultimos_pagos_mensuales) self.env['rrhh.prestamo.linea'].create({ 'prestamo_id': self.id, 'mes': mes, 'anio': anio, 'monto': ultimos_pagos_mensuales }) contador += 1 return True def prestamos(self): if self.prestamo_ids: cantidad_nominas = 0 for nomina in self.prestamo_ids: if nomina.nomina_id: cantidad_nominas += 1 if cantidad_nominas == 0: self.prestamo_ids.unlink() self.generar_mensualidades() else: raise ValidationError( _('No puede volver a generar mensualidades, por que ya existen nominas asociadas a este prestamo.' )) else: self.generar_mensualidades() return True @api.multi def unlink(self): for prestamo in self: if not prestamo.estado == 'nuevo': raise UserError( _('No puede eliminar prestamo, por que ya existen nominas asociadas' )) return super(hr_prestamo, self).unlink()
class Channel(models.Model): """ A channel is a container of slides. """ _name = 'slide.channel' _description = 'Slide Channel' _inherit = [ 'mail.thread', 'website.seo.metadata', 'website.published.multi.mixin', 'rating.mixin' ] _order = 'sequence, id' def _default_access_token(self): return str(uuid.uuid4()) # description name = fields.Char('Name', translate=True, required=True) active = fields.Boolean(default=True) description = fields.Html('Description', translate=html_translate, sanitize_attributes=False) channel_type = fields.Selection([('documentation', 'Documentation'), ('training', 'Training')], string="Course type", default="documentation", required=True) sequence = fields.Integer(default=10, help='Display order') user_id = fields.Many2one('res.users', string='Responsible', default=lambda self: self.env.uid) tag_ids = fields.Many2many( 'slide.channel.tag', 'slide_channel_tag_rel', 'channel_id', 'tag_id', string='Tags', help='Used to categorize and filter displayed channels/courses') category_ids = fields.One2many('slide.category', 'channel_id', string="Categories") image = fields.Binary("Image", attachment=True) image_medium = fields.Binary("Medium image", attachment=True) image_small = fields.Binary("Small image", attachment=True) # slides: promote, statistics slide_ids = fields.One2many('slide.slide', 'channel_id', string="Slides") slide_partner_ids = fields.One2many( 'slide.slide.partner', 'channel_id', string="Slide User Data", groups='website.group_website_publisher') promote_strategy = fields.Selection([('none', 'No Featured Presentation'), ('latest', 'Latest Published'), ('most_voted', 'Most Voted'), ('most_viewed', 'Most Viewed'), ('custom', 'Featured Presentation')], string="Featuring Policy", default='most_voted', required=True) access_token = fields.Char("Security Token", copy=False, default=_default_access_token) nbr_presentations = fields.Integer('Number of Presentations', compute='_compute_slides_statistics', store=True) nbr_documents = fields.Integer('Number of Documents', compute='_compute_slides_statistics', store=True) nbr_videos = fields.Integer('Number of Videos', compute='_compute_slides_statistics', store=True) nbr_infographics = fields.Integer('Number of Infographics', compute='_compute_slides_statistics', store=True) nbr_webpages = fields.Integer("Number of Webpages", compute='_compute_slides_statistics', store=True) total_slides = fields.Integer('# Slides', compute='_compute_slides_statistics', store=True, oldname='total') total_views = fields.Integer('# Views', compute='_compute_slides_statistics', store=True) total_votes = fields.Integer('# Votes', compute='_compute_slides_statistics', store=True) total_time = fields.Float('# Hours', compute='_compute_slides_statistics', digits=(10, 4), store=True) # configuration publish_template_id = fields.Many2one( 'mail.template', string='Published Template', help="Email template to send slide publication through email", default=lambda self: self.env['ir.model.data'].xmlid_to_res_id( 'website_slides.slide_template_published')) share_template_id = fields.Many2one( 'mail.template', string='Shared Template', help="Email template used when sharing a slide", default=lambda self: self.env['ir.model.data'].xmlid_to_res_id( 'website_slides.slide_template_shared')) visibility = fields.Selection([('public', 'Public'), ('invite', 'Invite')], default='public', required=True) partner_ids = fields.Many2many('res.partner', 'slide_channel_partner', 'channel_id', 'partner_id', string='Members', help="All members of the channel.", context={'active_test': False}) members_count = fields.Integer('Attendees count', compute='_compute_members_count') is_member = fields.Boolean(string='Is Member', compute='_compute_is_member') channel_partner_ids = fields.One2many( 'slide.channel.partner', 'channel_id', string='Members Information', groups='website.group_website_publisher') enroll_msg = fields.Html('Enroll Message', help="Message explaining the enroll process", default=False, translate=html_translate, sanitize_attributes=False) upload_group_ids = fields.Many2many( 'res.groups', 'rel_upload_groups', 'channel_id', 'group_id', string='Upload Groups', help= "Groups allowed to upload presentations in this channel. If void, every user can upload." ) # not stored access fields, depending on each user completed = fields.Boolean('Done', compute='_compute_user_statistics') completion = fields.Integer('Completion', compute='_compute_user_statistics') can_upload = fields.Boolean('Can Upload', compute='_compute_access') can_publish = fields.Boolean('Can Publish', compute='_compute_access') @api.depends('channel_partner_ids.channel_id') def _compute_members_count(self): read_group_res = self.env['slide.channel.partner'].sudo().read_group( [('channel_id', 'in', self.ids)], [], 'channel_id') data = dict((res['channel_id'][0], res['channel_id_count']) for res in read_group_res) for channel in self: channel.members_count = data.get(channel.id, 0) @api.depends('channel_partner_ids.partner_id') @api.model def _compute_is_member(self): channel_partners = self.env['slide.channel.partner'].sudo().search([ ('channel_id', 'in', self.ids), ]) result = dict() for cp in channel_partners: result.setdefault(cp.channel_id.id, []).append(cp.partner_id.id) for channel in self: channel.valid_channel_partner_ids = result.get(channel.id, False) channel.is_member = self.env.user.partner_id.id in channel.valid_channel_partner_ids if channel.valid_channel_partner_ids else False @api.depends('slide_ids.slide_type', 'slide_ids.is_published', 'slide_ids.likes', 'slide_ids.dislikes', 'slide_ids.total_views') def _compute_slides_statistics(self): result = dict.fromkeys( self.ids, dict(nbr_presentations=0, nbr_documents=0, nbr_videos=0, nbr_infographics=0, nbr_webpages=0, total_slides=0, total_views=0, total_votes=0, total_time=0)) read_group_res = self.env['slide.slide'].read_group( [('is_published', '=', True), ('channel_id', 'in', self.ids)], [ 'channel_id', 'slide_type', 'likes', 'dislikes', 'total_views', 'completion_time' ], groupby=['channel_id', 'slide_type'], lazy=False) for res_group in read_group_res: cid = res_group['channel_id'][0] result[cid]['nbr_presentations'] += res_group.get( 'slide_type', '') == 'presentation' and res_group['__count'] or 0 result[cid]['nbr_documents'] += res_group.get( 'slide_type', '') == 'document' and res_group['__count'] or 0 result[cid]['nbr_videos'] += res_group.get( 'slide_type', '') == 'video' and res_group['__count'] or 0 result[cid]['nbr_infographics'] += res_group.get( 'slide_type', '') == 'infographic' and res_group['__count'] or 0 result[cid]['nbr_webpages'] += res_group.get( 'slide_type', '') == 'webpage' and res_group['__count'] or 0 result[cid]['total_slides'] += res_group['__count'] result[cid]['total_views'] += res_group.get('total_views', 0) result[cid]['total_votes'] += res_group.get('likes', 0) result[cid]['total_votes'] -= res_group.get('dislikes', 0) result[cid]['total_time'] += res_group.get('completion_time', 0) for record in self: record.update(result[record.id]) @api.depends('slide_partner_ids') def _compute_user_statistics(self): current_user_info = self.env['slide.channel.partner'].sudo().search([ ('channel_id', 'in', self.ids), ('partner_id', '=', self.env.user.partner_id.id) ]) mapped_data = dict( (info.channel_id.id, (info.completed, info.completion)) for info in current_user_info) for record in self: record.completed, record.completion = mapped_data.get( record.id, (False, 0)) @api.depends('channel_type', 'partner_ids', 'upload_group_ids') def _compute_access(self): for record in self: is_doc = record.channel_type == 'documentation' record.can_upload = not self.env.user.share and ( not record.upload_group_ids or bool(record.upload_group_ids & self.env.user.groups_id)) record.can_publish = record.can_upload and self.env.user.has_group( 'website.group_website_publisher') and ( is_doc or record.user_id == self.env.user) @api.multi @api.depends('name') def _compute_website_url(self): super(Channel, self)._compute_website_url() base_url = self.env['ir.config_parameter'].sudo().get_param( 'web.base.url') for channel in self: if channel.id: # avoid to perform a slug on a not yet saved record in case of an onchange. channel.website_url = '%s/slides/%s' % (base_url, slug(channel)) @api.onchange('visibility') def change_visibility(self): pass @api.multi def action_redirect_to_members(self): action = self.env.ref( 'website_slides.slide_channel_partner_action').read()[0] action['view_mode'] = 'tree' action['domain'] = [('channel_id', 'in', self.ids)] if len(self) == 1: action['context'] = {'default_channel_id': self.id} return action @api.multi def action_channel_invite(self): self.ensure_one() if self.visibility != 'invite': raise UserError( _("You cannot send invitations for channels that are not set as 'invite'." )) template = self.env.ref( 'website_slides.mail_template_slide_channel_invite', raise_if_not_found=False) local_context = dict( self.env.context, default_channel_id=self.id, default_use_template=bool(template), default_template_id=template and template.id or False, notif_layout='mail.mail_notification_light', ) return { 'type': 'ir.actions.act_window', 'view_type': 'form', 'view_mode': 'form', 'res_model': 'slide.channel.invite', 'target': 'new', 'context': local_context, } # --------------------------------------------------------- # ORM Overrides # --------------------------------------------------------- @api.model_cr_context def _init_column(self, column_name): """ Initialize the value of the given column for existing rows. Overridden here because we need to generate different access tokens and by default _init_column calls the default method once and applies it for every record. """ if column_name != 'access_token': super(Channel, self)._init_column(column_name) else: query = """ UPDATE %(table_name)s SET %(column_name)s = md5(md5(random()::varchar || id::varchar) || clock_timestamp()::varchar)::uuid::varchar WHERE %(column_name)s IS NULL """ % { 'table_name': self._table, 'column_name': column_name } self.env.cr.execute(query) @api.model def create(self, vals): # Ensure creator is member of its channel it is easier for him to manage it if not vals.get('channel_partner_ids'): vals['channel_partner_ids'] = [(0, 0, { 'partner_id': self.env.user.partner_id.id })] if 'image' in vals: tools.image_resize_images(vals) return super( Channel, self.with_context(mail_create_nosubscribe=True)).create(vals) @api.multi def write(self, vals): if 'image' in vals: tools.image_resize_images(vals) res = super(Channel, self).write(vals) if 'active' in vals: # archiving/unarchiving a channel does it on its slides, too self.with_context(active_test=False).mapped('slide_ids').write( {'active': vals['active']}) return res @api.multi @api.returns('mail.message', lambda value: value.id) def message_post(self, parent_id=False, subtype=None, **kwargs): """ Temporary workaround to avoid spam. If someone replies on a channel through the 'Presentation Published' email, it should be considered as a note as we don't want all channel followers to be notified of this answer. """ self.ensure_one() if parent_id: parent_message = self.env['mail.message'].sudo().browse(parent_id) if parent_message.subtype_id and parent_message.subtype_id == self.env.ref( 'website_slides.mt_channel_slide_published'): if kwargs.get('subtype_id'): kwargs['subtype_id'] = False subtype = 'mail.mt_note' return super(Channel, self).message_post(parent_id=parent_id, subtype=subtype, **kwargs) # --------------------------------------------------------- # Rating Mixin API # --------------------------------------------------------- def action_add_member(self, **member_values): """ Adds the logged in user in the channel members. (see '_action_add_member' for more info) Returns True if added successfully, False otherwise.""" return bool( self._action_add_member(target_partner=self.env.user.partner_id, **member_values)) def _action_add_member(self, target_partner=False, **member_values): """ Add the target_partner as a member of the channel (to its slide.channel.partner). This will make the content (slides) of the channel available to that partner. Returns the added 'slide.channel.partner's (! as sudo !) """ existing = self.env['slide.channel.partner'].sudo().search([ ('channel_id', 'in', self.ids), ('partner_id', '=', target_partner.id) ]) to_join = (self - existing.mapped('channel_id'))._filter_add_member( target_partner, **member_values) if to_join: slide_partners_sudo = self.env['slide.channel.partner'].sudo( ).create([ dict(channel_id=channel.id, partner_id=target_partner.id, **member_values) for channel in to_join ]) return slide_partners_sudo return self.env['slide.channel.partner'].sudo() def _filter_add_member(self, target_partner, **member_values): allowed = self.filtered(lambda channel: channel.visibility == 'public') on_invite = self.filtered( lambda channel: channel.visibility == 'invite') if on_invite: try: on_invite.check_access_rights('write') on_invite.check_access_rule('write') except: pass else: allowed |= on_invite return allowed def list_all(self): return { 'channels': [{ 'id': channel.id, 'name': channel.name, 'website_url': channel.website_url } for channel in self.search([])] } # --------------------------------------------------------- # Rating Mixin API # --------------------------------------------------------- @api.multi def _rating_domain(self): """ Only take the published rating into account to compute avg and count """ domain = super(Channel, self)._rating_domain() return expression.AND([domain, [('website_published', '=', True)]])
class ResPartner(models.Model): _inherit = "res.partner" arba_alicuot_ids = fields.One2many( 'res.partner.arba_alicuot', 'partner_id', 'Alicuotas de ARBA', ) drei = fields.Selection( [ ('activo', 'Activo'), ('no_activo', 'No Activo'), ], string='DREI', ) # TODO agregarlo en mig a v10 ya que fix dbs no es capaz de arreglarlo # porque da el error antes de empezar a arreglar # drei_number = fields.Char( # ) default_regimen_ganancias_id = fields.Many2one( 'afip.tabla_ganancias.alicuotasymontos', 'Regimen Ganancias por Defecto', ) @api.multi def get_arba_alicuota_percepcion(self, alicuot_no_inscripto=False): company = self._context.get('invoice_company') date_invoice = self._context.get('date_invoice') if date_invoice and company: date = fields.Date.from_string(date_invoice) arba = self.get_arba_data(company, date) # si pasamos alicuota para no inscripto y no hay numero de # comprobante entonces es porque no figura en el padron if alicuot_no_inscripto and not arba.numero_comprobante: return alicuot_no_inscripto return arba.alicuota_percepcion / 100.0 return 0 @api.multi def get_arba_alicuota_retencion(self, company, date): arba = self.get_arba_data(company, date) return arba.alicuota_retencion / 100.0 @api.multi def get_arba_data(self, company, date): self.ensure_one() from_date = (date + relativedelta(day=1)).strftime('%Y%m%d') to_date = (date + relativedelta(day=1, days=-1, months=+1)).strftime('%Y%m%d') commercial_partner = self.commercial_partner_id arba = self.arba_alicuot_ids.search( [('from_date', '=', from_date), ('to_date', '=', to_date), ('company_id', '=', company.id), ('partner_id', '=', commercial_partner.id)], limit=1) if not arba: arba_data = company.get_arba_data( commercial_partner, from_date, to_date, ) arba_data['partner_id'] = commercial_partner.id arba_data['company_id'] = company.id arba = self.arba_alicuot_ids.sudo().create(arba_data) return arba
class Erups(models.Model): _name = 'erups' name = fields.Char(string='Nama Kegiatan', required=True) emitten = fields.Char() event_date = fields.Date(string='Tanggal Event') location = fields.Text(string='Lokasi Event') agenda_ids = fields.One2many('erups_agenda', 'erups_id') description = fields.Text() status = fields.Selection([('open', 'Open'), ('close', 'Closed')], required=True, default="open") total_agenda = fields.Integer('Jumlah Agenda', compute='_compute_total_agenda') total_question = fields.Integer('Jumlah Pertanyaan', compute='_compute_total_question') total_question_valid = fields.Integer('Jumlah Pertanyaan Valid', compute='_compute_total_valid') total_question_relevan = fields.Integer('Jumlah Pertanyaan Relevan', compute='_compute_total_relevan') total_question_verified = fields.Integer('Jumlah Pertanyaan Dipilih', compute='_compute_total_verified') @api.depends('agenda_ids') def _compute_total_agenda(self): for r in self: if r.agenda_ids: r.total_agenda = len(r.agenda_ids) else: r.total_agenda = 0 @api.depends('agenda_ids') def _compute_total_question(self): for r in self: if r.agenda_ids: for a in r.agenda_ids: r.total_question += len(a.question_ids) else: r.total_question = 0 @api.depends('agenda_ids') def _compute_total_valid(self): for r in self: r.total_question_valid = 0 if r.agenda_ids: for a in r.agenda_ids: if a.question_ids: for q in a.question_ids: if q.status not in ['shareholder', 'bae_reject']: r.total_question_valid += 1 @api.depends('agenda_ids') def _compute_total_relevan(self): for r in self: r.total_question_relevan = 0 if r.agenda_ids: for a in r.agenda_ids: if a.question_ids: for q in a.question_ids: if q.status in ['consultant', 'speaker']: r.total_question_relevan += 1 @api.depends('agenda_ids') def _compute_total_verified(self): for r in self: if r.agenda_ids: for a in r.agenda_ids: r.total_question_verified = a.search_count( [['question_ids.status', '=', 'speaker']]) else: r.total_question_verified = 0
class Agenda(models.Model): _name = 'erups_agenda' # _rec_name = 'complete_name' name = fields.Char(string='Nama Agenda', required=True) agenda_num = fields.Integer(string='No Urut') description = fields.Text() erups_id = fields.Many2one('erups', ondelete='cascade', required=True) status = fields.Selection([('open', 'Open'), ('close', 'Closed'), ('done', 'Done')], required=True, default="open") question_ids = fields.One2many('erups_question', 'agenda_id') total_question = fields.Integer('Jumlah Pertanyaan', compute='_compute_total_question') total_question_valid = fields.Integer('Jumlah Pertanyaan Valid', compute='_compute_total_valid') total_question_not_valid = fields.Integer( 'Jumlah Pertanyaan Tidak Valid', compute='_compute_total_not_valid') # total_notaris_relevan = fields.Integer('Jumlah Pertanyaan Notaris Relevan', compute='_compute_total_notaris_relevan') # total_notaris_not_relevan = fields.Integer('Jumlah Pertanyaan Notaris Not Relevan', # compute='_compute_total_notaris_not_relevan') total_consultant_relevan = fields.Integer( 'Jumlah Pertanyaan Konsultan Relevan', compute='_compute_total_consultant_relevan') total_consultant_not_relevan = fields.Integer( 'Jumlah Pertanyaan Konsultan Not Relevan', compute='_compute_total_consultant_not_relevan') total_question_relevan = fields.Integer( 'Jumlah Pertanyaan Valid dan Relevan', compute='_compute_total_relevan') total_question_verified = fields.Integer('Jumlah Pertanyaan Dipilih', compute='_compute_total_verified') complete_name = fields.Char("Agenda Full Name", compute='_compute_complete_name', store=True) num_name = fields.Char("Agenda Name", compute='_compute_num_name', store=True) relevan_question_ids = fields.One2many('erups_question', 'agenda_id', domain=[('status', 'in', ['consultant', 'speaker'])]) speaker_question_ids = fields.One2many('erups_question', 'agenda_id', domain=[('status', 'in', ['speaker'])]) active_agenda = fields.Integer(compute='_compute_active_agenda') agenda_num_text = fields.Char(compute='_compute_num_to_text') @api.depends('agenda_num') def _compute_active_agenda(self): ErupsAgenda = self.env['erups_agenda'].sudo().search( [('status', 'not in', ['done'])], order='agenda_num', limit=1) first_agenda_open = ErupsAgenda.agenda_num for r in self: r.active_agenda = first_agenda_open @api.depends('agenda_num') def _compute_num_to_text(self): for r in self: if r.agenda_num == 1: r.agenda_num_text = "Pertama" if r.agenda_num == 2: r.agenda_num_text = "Kedua" if r.agenda_num == 3: r.agenda_num_text = "Ketiga" if r.agenda_num == 4: r.agenda_num_text = "Keempat" if r.agenda_num == 5: r.agenda_num_text = "Kelima" if r.agenda_num == 6: r.agenda_num_text = "Keenam" @api.depends('question_ids') def _compute_total_question(self): for r in self: if r.question_ids: r.total_question = len(r.question_ids) else: r.total_question = 0 @api.depends('question_ids') def _compute_total_valid(self): for r in self: r.total_question_valid = 0 if r.question_ids: for q in r.question_ids: if q.status not in ['shareholder', 'bae_reject']: r.total_question_valid += 1 else: r.total_question_valid = 0 @api.depends('question_ids') def _compute_total_not_valid(self): for r in self: r.total_question_not_valid = 0 if r.question_ids: for q in r.question_ids: if q.status in ['bae_reject', 'shareholder']: r.total_question_not_valid += 1 @api.depends('question_ids') def _compute_total_notaris_relevan(self): for r in self: r.total_notaris_relevan = 0 if r.question_ids: for q in r.question_ids: if q.status == 'notaris': r.total_notaris_relevan += 1 @api.depends('question_ids') def _compute_total_notaris_not_relevan(self): for r in self: r.total_notaris_not_relevan = 0 if r.question_ids: for q in r.question_ids: if q.status == 'notaris_reject': r.total_notaris_not_relevan += 1 @api.depends('question_ids') def _compute_total_consultant_relevan(self): for r in self: r.total_consultant_relevan = 0 if r.question_ids: for q in r.question_ids: if q.status in ['consultant', 'speaker']: r.total_consultant_relevan += 1 @api.depends('question_ids') def _compute_total_consultant_not_relevan(self): for r in self: r.total_consultant_not_relevan = 0 if r.question_ids: for q in r.question_ids: if q.status in ['consultant_reject', 'bae']: r.total_consultant_not_relevan += 1 @api.depends('question_ids') def _compute_total_relevan(self): for r in self: r.total_question_relevan = 0 if r.question_ids: for q in r.question_ids: if q.status in ['consultant', 'speaker']: r.total_question_relevan += 1 else: r.total_question_relevan = 0 @api.depends('question_ids') def _compute_total_verified(self): for r in self: if r.question_ids: for q in r.question_ids: if q.status == 'speaker': r.total_question_verified += 1 else: r.total_question_verified = 0 @api.depends('name', 'agenda_num') def _compute_complete_name(self): for r in self: if r.agenda_num: r.complete_name = 'Agenda %s - %s' % (r.agenda_num, r.name) else: r.complete_name = r.name @api.depends('name', 'agenda_num') def _compute_num_name(self): for r in self: if r.agenda_num: r.num_name = 'Agenda %s' % (r.agenda_num) else: r.num_name = r.name # @api.onchange('status') # def _onchange_status(self): # print('merdeka', self) # if self.status == 'open': # agenda_open = self.env['erups_agenda'].sudo().search([('status', '=', 'open')]) # agenda_open.sudo().write({'status': 'close'}) # # self.sudo().write({'status': 'open'}) def open_agenda(self): agenda_open = self.env['erups_agenda'].sudo().search([('status', '=', 'open')]) agenda_open.sudo().write({'status': 'close'}) for r in self: r.sudo().write({'status': 'open'}) return True def close_agenda(self): for r in self: r.sudo().write({'status': 'close'}) return True def done_agenda(self): for r in self: r.sudo().write({'status': 'done'}) return True
class MassMailing(models.Model): """ MassMailing models a wave of emails for a mass mailign campaign. A mass mailing is an occurence of sending emails. """ _name = 'mail.mass_mailing' _description = 'Mass Mailing' # number of periods for tracking mail_mail statistics _period_number = 6 _order = 'sent_date DESC' _inherits = {'utm.source': 'source_id'} _rec_name = "source_id" @api.model def _get_default_mail_server_id(self): server_id = self.env['ir.config_parameter'].sudo().get_param('mass_mailing.mail_server_id') try: server_id = literal_eval(server_id) if server_id else False return self.env['ir.mail_server'].search([('id', '=', server_id)]).id except ValueError: return False @api.model def default_get(self, fields): res = super(MassMailing, self).default_get(fields) if 'reply_to_mode' in fields and not 'reply_to_mode' in res and res.get('mailing_model_real'): if res['mailing_model_real'] in ['res.partner', 'mail.mass_mailing.contact']: res['reply_to_mode'] = 'email' else: res['reply_to_mode'] = 'thread' return res active = fields.Boolean(default=True) email_from = fields.Char(string='From', required=True, default=lambda self: self.env['mail.message']._get_default_from()) sent_date = fields.Datetime(string='Sent Date', oldname='date', copy=False) schedule_date = fields.Datetime(string='Schedule in the Future') body_html = fields.Html(string='Body', sanitize_attributes=False) attachment_ids = fields.Many2many('ir.attachment', 'mass_mailing_ir_attachments_rel', 'mass_mailing_id', 'attachment_id', string='Attachments') keep_archives = fields.Boolean(string='Keep Archives') mass_mailing_campaign_id = fields.Many2one('mail.mass_mailing.campaign', string='Mass Mailing Campaign') campaign_id = fields.Many2one('utm.campaign', string='Campaign', help="This name helps you tracking your different campaign efforts, e.g. Fall_Drive, Christmas_Special") source_id = fields.Many2one('utm.source', string='Subject', required=True, ondelete='cascade', help="This is the link source, e.g. Search Engine, another domain, or name of email list") medium_id = fields.Many2one('utm.medium', string='Medium', help="This is the delivery method, e.g. Postcard, Email, or Banner Ad", default=lambda self: self.env.ref('utm.utm_medium_email')) clicks_ratio = fields.Integer(compute="_compute_clicks_ratio", string="Number of Clicks") state = fields.Selection([('draft', 'Draft'), ('in_queue', 'In Queue'), ('sending', 'Sending'), ('done', 'Sent')], string='Status', required=True, copy=False, default='draft', group_expand='_group_expand_states') color = fields.Integer(string='Color Index') user_id = fields.Many2one('res.users', string='Mailing Manager', default=lambda self: self.env.user) # mailing options reply_to_mode = fields.Selection( [('thread', 'Recipient Followers'), ('email', 'Specified Email Address')], string='Reply-To Mode', required=True) reply_to = fields.Char(string='Reply To', help='Preferred Reply-To Address', default=lambda self: self.env['mail.message']._get_default_from()) # recipients mailing_model_real = fields.Char(compute='_compute_model', string='Recipients Real Model', default='mail.mass_mailing.contact', required=True) mailing_model_id = fields.Many2one('ir.model', string='Recipients Model', domain=[('model', 'in', MASS_MAILING_BUSINESS_MODELS)], default=lambda self: self.env.ref('mass_mailing.model_mail_mass_mailing_list').id) mailing_model_name = fields.Char(related='mailing_model_id.model', string='Recipients Model Name', readonly=True, related_sudo=True) mailing_domain = fields.Char(string='Domain', oldname='domain', default=[]) mail_server_id = fields.Many2one('ir.mail_server', string='Mail Server', default=_get_default_mail_server_id, help="Use a specific mail server in priority. Otherwise ALWAFI relies on the first outgoing mail server available (based on their sequencing) as it does for normal mails.") contact_list_ids = fields.Many2many('mail.mass_mailing.list', 'mail_mass_mailing_list_rel', string='Mailing Lists') contact_ab_pc = fields.Integer(string='A/B Testing percentage', help='Percentage of the contacts that will be mailed. Recipients will be taken randomly.', default=100) # statistics data statistics_ids = fields.One2many('mail.mail.statistics', 'mass_mailing_id', string='Emails Statistics') total = fields.Integer(compute="_compute_total") scheduled = fields.Integer(compute="_compute_statistics") expected = fields.Integer(compute="_compute_statistics") ignored = fields.Integer(compute="_compute_statistics") sent = fields.Integer(compute="_compute_statistics") delivered = fields.Integer(compute="_compute_statistics") opened = fields.Integer(compute="_compute_statistics") clicked = fields.Integer(compute="_compute_statistics") replied = fields.Integer(compute="_compute_statistics") bounced = fields.Integer(compute="_compute_statistics") failed = fields.Integer(compute="_compute_statistics") received_ratio = fields.Integer(compute="_compute_statistics", string='Received Ratio') opened_ratio = fields.Integer(compute="_compute_statistics", string='Opened Ratio') replied_ratio = fields.Integer(compute="_compute_statistics", string='Replied Ratio') bounced_ratio = fields.Integer(compute="_compute_statistics", String='Bounced Ratio') next_departure = fields.Datetime(compute="_compute_next_departure", string='Scheduled date') def _compute_total(self): for mass_mailing in self: mass_mailing.total = len(mass_mailing.sudo().get_recipients()) def _compute_clicks_ratio(self): self.env.cr.execute(""" SELECT COUNT(DISTINCT(stats.id)) AS nb_mails, COUNT(DISTINCT(clicks.mail_stat_id)) AS nb_clicks, stats.mass_mailing_id AS id FROM mail_mail_statistics AS stats LEFT OUTER JOIN link_tracker_click AS clicks ON clicks.mail_stat_id = stats.id WHERE stats.mass_mailing_id IN %s GROUP BY stats.mass_mailing_id """, (tuple(self.ids), )) mass_mailing_data = self.env.cr.dictfetchall() mapped_data = dict([(m['id'], 100 * m['nb_clicks'] / m['nb_mails']) for m in mass_mailing_data]) for mass_mailing in self: mass_mailing.clicks_ratio = mapped_data.get(mass_mailing.id, 0) @api.depends('mailing_model_id') def _compute_model(self): for record in self: record.mailing_model_real = (record.mailing_model_name != 'mail.mass_mailing.list') and record.mailing_model_name or 'mail.mass_mailing.contact' def _compute_statistics(self): """ Compute statistics of the mass mailing """ self.env.cr.execute(""" SELECT m.id as mailing_id, COUNT(s.id) AS expected, COUNT(CASE WHEN s.sent is not null THEN 1 ELSE null END) AS sent, COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is null AND s.ignored is null THEN 1 ELSE null END) AS scheduled, COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is not null THEN 1 ELSE null END) AS failed, COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is null AND s.ignored is not null THEN 1 ELSE null END) AS ignored, COUNT(CASE WHEN s.sent is not null AND s.bounced is null THEN 1 ELSE null END) AS delivered, COUNT(CASE WHEN s.opened is not null THEN 1 ELSE null END) AS opened, COUNT(CASE WHEN s.clicked is not null THEN 1 ELSE null END) AS clicked, COUNT(CASE WHEN s.replied is not null THEN 1 ELSE null END) AS replied, COUNT(CASE WHEN s.bounced is not null THEN 1 ELSE null END) AS bounced, COUNT(CASE WHEN s.exception is not null THEN 1 ELSE null END) AS failed FROM mail_mail_statistics s RIGHT JOIN mail_mass_mailing m ON (m.id = s.mass_mailing_id) WHERE m.id IN %s GROUP BY m.id """, (tuple(self.ids), )) for row in self.env.cr.dictfetchall(): total = row['expected'] = (row['expected'] - row['ignored']) or 1 row['received_ratio'] = 100.0 * row['delivered'] / total row['opened_ratio'] = 100.0 * row['opened'] / total row['clicks_ratio'] = 100.0 * row['clicked'] / total row['replied_ratio'] = 100.0 * row['replied'] / total row['bounced_ratio'] = 100.0 * row['bounced'] / total self.browse(row.pop('mailing_id')).update(row) @api.multi def _unsubscribe_token(self, res_id, email): """Generate a secure hash for this mailing list and parameters. This is appended to the unsubscription URL and then checked at unsubscription time to ensure no malicious unsubscriptions are performed. :param int res_id: ID of the resource that will be unsubscribed. :param str email: Email of the resource that will be unsubscribed. """ secret = self.env["ir.config_parameter"].sudo().get_param( "database.secret") token = (self.env.cr.dbname, self.id, int(res_id), tools.ustr(email)) return hmac.new(secret.encode('utf-8'), repr(token).encode('utf-8'), hashlib.sha512).hexdigest() def _compute_next_departure(self): cron_next_call = self.env.ref('mass_mailing.ir_cron_mass_mailing_queue').sudo().nextcall str2dt = fields.Datetime.from_string cron_time = str2dt(cron_next_call) for mass_mailing in self: if mass_mailing.schedule_date: schedule_date = str2dt(mass_mailing.schedule_date) mass_mailing.next_departure = max(schedule_date, cron_time) else: mass_mailing.next_departure = cron_time @api.onchange('mass_mailing_campaign_id') def _onchange_mass_mailing_campaign_id(self): if self.mass_mailing_campaign_id: dic = {'campaign_id': self.mass_mailing_campaign_id.campaign_id, 'source_id': self.mass_mailing_campaign_id.source_id, 'medium_id': self.mass_mailing_campaign_id.medium_id} self.update(dic) @api.onchange('mailing_model_id', 'contact_list_ids') def _onchange_model_and_list(self): mailing_domain = [] if self.mailing_model_name: if self.mailing_model_name == 'mail.mass_mailing.list': if self.contact_list_ids: mailing_domain.append(('list_ids', 'in', self.contact_list_ids.ids)) else: mailing_domain.append((0, '=', 1)) elif self.mailing_model_name == 'res.partner': mailing_domain.append(('customer', '=', True)) elif 'opt_out' in self.env[self.mailing_model_name]._fields and not self.mailing_domain: mailing_domain.append(('opt_out', '=', False)) else: mailing_domain.append((0, '=', 1)) self.mailing_domain = repr(mailing_domain) self.body_html = "on_change_model_and_list" #------------------------------------------------------ # Technical stuff #------------------------------------------------------ @api.model def name_create(self, name): """ _rec_name is source_id, creates a utm.source instead """ mass_mailing = self.create({'name': name}) return mass_mailing.name_get()[0] @api.multi @api.returns('self', lambda value: value.id) def copy(self, default=None): self.ensure_one() default = dict(default or {}, name=_('%s (copy)') % self.name) return super(MassMailing, self).copy(default=default) def _group_expand_states(self, states, domain, order): return [key for key, val in type(self).state.selection] def update_opt_out(self, email, list_ids, value): if len(list_ids) > 0: model = self.env['mail.mass_mailing.contact'].with_context(active_test=False) records = model.search([('email', '=ilike', email)]) opt_out_records = self.env['mail.mass_mailing.list_contact_rel'].search([ ('contact_id', 'in', records.ids), ('list_id', 'in', list_ids), ('opt_out', '!=', value) ]) opt_out_records.write({'opt_out': value}) message = _('The recipient <strong>unsubscribed from %s</strong> mailing list(s)') \ if value else _('The recipient <strong>subscribed to %s</strong> mailing list(s)') for record in records: # filter the list_id by record record_lists = opt_out_records.filtered(lambda rec: rec.contact_id.id == record.id) if len(record_lists) > 0: record.sudo().message_post(body=_(message % ', '.join(str(list.name) for list in record_lists.mapped('list_id')))) @api.model def create(self, values): if values.get('body_html'): values['body_html'] = self._convert_inline_images_to_urls(values['body_html']) return super(MassMailing, self).create(values) def write(self, values): if values.get('body_html'): values['body_html'] = self._convert_inline_images_to_urls(values['body_html']) return super(MassMailing, self).write(values) def _convert_inline_images_to_urls(self, body_html): """ Find inline base64 encoded images, make an attachement out of them and replace the inline image with an url to the attachement. """ def _image_to_url(b64image: bytes): """Store an image in an attachement and returns an url""" attachment = self.env['ir.attachment'].create({ 'name': "cropped_image", 'datas': b64image, 'datas_fname': "cropped_image_mailing_{}".format(self.id), 'type': 'binary',}) attachment.generate_access_token() return '/web/image/%s?access_token=%s' % ( attachment.id, attachment.access_token) modified = False root = lxml.html.fromstring(body_html) for node in root.iter('img'): match = image_re.match(node.attrib.get('src', '')) if match: mime = match.group(1) # unsed image = match.group(2).encode() # base64 image as bytes node.attrib['src'] = _image_to_url(image) modified = True if modified: return lxml.html.tostring(root) return body_html #------------------------------------------------------ # Views & Actions #------------------------------------------------------ @api.multi def action_duplicate(self): self.ensure_one() mass_mailing_copy = self.copy() if mass_mailing_copy: context = dict(self.env.context) context['form_view_initial_mode'] = 'edit' return { 'type': 'ir.actions.act_window', 'view_type': 'form', 'view_mode': 'form', 'res_model': 'mail.mass_mailing', 'res_id': mass_mailing_copy.id, 'context': context, } return False @api.multi def action_test_mailing(self): self.ensure_one() ctx = dict(self.env.context, default_mass_mailing_id=self.id) return { 'name': _('Test Mailing'), 'type': 'ir.actions.act_window', 'view_mode': 'form', 'res_model': 'mail.mass_mailing.test', 'target': 'new', 'context': ctx, } @api.multi def action_schedule_date(self): self.ensure_one() action = self.env.ref('mass_mailing.mass_mailing_schedule_date_action').read()[0] action['context'] = dict(self.env.context, default_mass_mailing_id=self.id) return action @api.multi def put_in_queue(self): self.write({'state': 'in_queue'}) @api.multi def cancel_mass_mailing(self): self.write({'state': 'draft', 'schedule_date': False}) @api.multi def retry_failed_mail(self): failed_mails = self.env['mail.mail'].search([('mailing_id', 'in', self.ids), ('state', '=', 'exception')]) failed_mails.mapped('statistics_ids').unlink() failed_mails.sudo().unlink() res_ids = self.get_recipients() except_mailed = self.env['mail.mail.statistics'].search([ ('model', '=', self.mailing_model_real), ('res_id', 'in', res_ids), ('exception', '!=', False), ('mass_mailing_id', '=', self.id)]).unlink() self.write({'state': 'in_queue'}) def action_view_sent(self): return self._action_view_documents_filtered('sent') def action_view_opened(self): return self._action_view_documents_filtered('opened') def action_view_replied(self): return self._action_view_documents_filtered('replied') def action_view_bounced(self): return self._action_view_documents_filtered('bounced') def action_view_clicked(self): return self._action_view_documents_filtered('clicked') def action_view_delivered(self): return self._action_view_documents_filtered('delivered') def _action_view_documents_filtered(self, view_filter): if view_filter in ('sent', 'opened', 'replied', 'bounced', 'clicked'): opened_stats = self.statistics_ids.filtered(lambda stat: stat[view_filter]) elif view_filter == ('delivered'): opened_stats = self.statistics_ids.filtered(lambda stat: stat.sent and not stat.bounced) else: opened_stats = self.env['mail.mail.statistics'] res_ids = opened_stats.mapped('res_id') model_name = self.env['ir.model']._get(self.mailing_model_real).display_name return { 'name': model_name, 'type': 'ir.actions.act_window', 'view_mode': 'tree', 'res_model': self.mailing_model_real, 'domain': [('id', 'in', res_ids)], } #------------------------------------------------------ # Email Sending #------------------------------------------------------ def _get_opt_out_list(self): """Returns a set of emails opted-out in target model""" self.ensure_one() opt_out = {} target = self.env[self.mailing_model_real] if self.mailing_model_real == "mail.mass_mailing.contact": # if user is opt_out on One list but not on another # or if two user with same email address, one opted in and the other one opted out, send the mail anyway # TODO DBE Fixme : Optimise the following to get real opt_out and opt_in target_list_contacts = self.env['mail.mass_mailing.list_contact_rel'].search( [('list_id', 'in', self.contact_list_ids.ids)]) opt_out_contacts = target_list_contacts.filtered(lambda rel: rel.opt_out).mapped('contact_id.email') opt_in_contacts = target_list_contacts.filtered(lambda rel: not rel.opt_out).mapped('contact_id.email') normalized_email = [tools.email_split(c) for c in opt_out_contacts if c not in opt_in_contacts] opt_out = set(email[0].lower() for email in normalized_email if email) _logger.info( "Mass-mailing %s targets %s, blacklist: %s emails", self, target._name, len(opt_out)) else: _logger.info("Mass-mailing %s targets %s, no opt out list available", self, target._name) return opt_out def _get_convert_links(self): self.ensure_one() utm_mixin = self.mass_mailing_campaign_id if self.mass_mailing_campaign_id else self vals = {'mass_mailing_id': self.id} if self.mass_mailing_campaign_id: vals['mass_mailing_campaign_id'] = self.mass_mailing_campaign_id.id if utm_mixin.campaign_id: vals['campaign_id'] = utm_mixin.campaign_id.id if utm_mixin.source_id: vals['source_id'] = utm_mixin.source_id.id if utm_mixin.medium_id: vals['medium_id'] = utm_mixin.medium_id.id return vals def _get_seen_list(self): """Returns a set of emails already targeted by current mailing/campaign (no duplicates)""" self.ensure_one() target = self.env[self.mailing_model_real] if set(['email', 'email_from']) & set(target._fields): mail_field = 'email' if 'email' in target._fields else 'email_from' # avoid loading a large number of records in memory # + use a basic heuristic for extracting emails query = """ SELECT lower(substring(t.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)')) FROM mail_mail_statistics s JOIN %(target)s t ON (s.res_id = t.id) WHERE substring(t.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)') IS NOT NULL """ elif 'partner_id' in target._fields: mail_field = 'email' query = """ SELECT lower(substring(p.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)')) FROM mail_mail_statistics s JOIN %(target)s t ON (s.res_id = t.id) JOIN res_partner p ON (t.partner_id = p.id) WHERE substring(p.%(mail_field)s, '([^ ,;<@]+@[^> ,;]+)') IS NOT NULL """ else: raise UserError(_("Unsupported mass mailing model %s") % self.mailing_model_id.name) if self.mass_mailing_campaign_id.unique_ab_testing: query +=""" AND s.mass_mailing_campaign_id = %%(mailing_campaign_id)s; """ else: query +=""" AND s.mass_mailing_id = %%(mailing_id)s AND s.model = %%(target_model)s; """ query = query % {'target': target._table, 'mail_field': mail_field} params = {'mailing_id': self.id, 'mailing_campaign_id': self.mass_mailing_campaign_id.id, 'target_model': self.mailing_model_real} self._cr.execute(query, params) seen_list = set(m[0] for m in self._cr.fetchall()) _logger.info( "Mass-mailing %s has already reached %s %s emails", self, len(seen_list), target._name) return seen_list def _get_mass_mailing_context(self): """Returns extra context items with pre-filled blacklist and seen list for massmailing""" return { 'mass_mailing_opt_out_list': self._get_opt_out_list(), 'mass_mailing_seen_list': self._get_seen_list(), 'post_convert_links': self._get_convert_links(), } def get_recipients(self): if self.mailing_domain: domain = safe_eval(self.mailing_domain) res_ids = self.env[self.mailing_model_real].search(domain).ids else: res_ids = [] domain = [('id', 'in', res_ids)] # randomly choose a fragment if self.contact_ab_pc < 100: contact_nbr = self.env[self.mailing_model_real].search_count(domain) topick = int(contact_nbr / 100.0 * self.contact_ab_pc) if self.mass_mailing_campaign_id and self.mass_mailing_campaign_id.unique_ab_testing: already_mailed = self.mass_mailing_campaign_id.get_recipients()[self.mass_mailing_campaign_id.id] else: already_mailed = set([]) remaining = set(res_ids).difference(already_mailed) if topick > len(remaining): topick = len(remaining) res_ids = random.sample(remaining, topick) return res_ids def get_remaining_recipients(self): res_ids = self.get_recipients() already_mailed = self.env['mail.mail.statistics'].search_read([('model', '=', self.mailing_model_real), ('res_id', 'in', res_ids), ('mass_mailing_id', '=', self.id)], ['res_id']) already_mailed_res_ids = [record['res_id'] for record in already_mailed] return list(set(res_ids) - set(already_mailed_res_ids)) def send_mail(self, res_ids=None): author_id = self.env.user.partner_id.id for mailing in self: if not res_ids: res_ids = mailing.get_remaining_recipients() if not res_ids: raise UserError(_('There is no recipients selected.')) composer_values = { 'author_id': author_id, 'attachment_ids': [(4, attachment.id) for attachment in mailing.attachment_ids], 'body': mailing.body_html, 'subject': mailing.name, 'model': mailing.mailing_model_real, 'email_from': mailing.email_from, 'record_name': False, 'composition_mode': 'mass_mail', 'mass_mailing_id': mailing.id, 'mailing_list_ids': [(4, l.id) for l in mailing.contact_list_ids], 'no_auto_thread': mailing.reply_to_mode != 'thread', 'template_id': None, 'mail_server_id': mailing.mail_server_id.id, } if mailing.reply_to_mode == 'email': composer_values['reply_to'] = mailing.reply_to composer = self.env['mail.compose.message'].with_context(active_ids=res_ids).create(composer_values) extra_context = self._get_mass_mailing_context() composer = composer.with_context(active_ids=res_ids, **extra_context) # auto-commit except in testing mode auto_commit = not getattr(threading.currentThread(), 'testing', False) composer.send_mail(auto_commit=auto_commit) mailing.write({'state': 'done', 'sent_date': fields.Datetime.now()}) return True def convert_links(self): res = {} for mass_mailing in self: utm_mixin = mass_mailing.mass_mailing_campaign_id if mass_mailing.mass_mailing_campaign_id else mass_mailing html = mass_mailing.body_html if mass_mailing.body_html else '' vals = {'mass_mailing_id': mass_mailing.id} if mass_mailing.mass_mailing_campaign_id: vals['mass_mailing_campaign_id'] = mass_mailing.mass_mailing_campaign_id.id if utm_mixin.campaign_id: vals['campaign_id'] = utm_mixin.campaign_id.id if utm_mixin.source_id: vals['source_id'] = utm_mixin.source_id.id if utm_mixin.medium_id: vals['medium_id'] = utm_mixin.medium_id.id res[mass_mailing.id] = self.env['link.tracker'].convert_links(html, vals, blacklist=['/unsubscribe_from_list']) return res @api.model def _process_mass_mailing_queue(self): mass_mailings = self.search([('state', 'in', ('in_queue', 'sending')), '|', ('schedule_date', '<', fields.Datetime.now()), ('schedule_date', '=', False)]) for mass_mailing in mass_mailings: user = mass_mailing.write_uid or self.env.user mass_mailing = mass_mailing.with_context(**user.sudo(user=user).context_get()) if len(mass_mailing.get_remaining_recipients()) > 0: mass_mailing.state = 'sending' mass_mailing.send_mail() else: mass_mailing.write({'state': 'done', 'sent_date': fields.Datetime.now()})
class MassMailingList(models.Model): """Model of a contact list. """ _name = 'mail.mass_mailing.list' _order = 'name' _description = 'Mailing List' name = fields.Char(string='Mailing List', required=True) active = fields.Boolean(default=True) contact_nbr = fields.Integer(compute="_compute_contact_nbr", string='Number of Contacts') contact_ids = fields.Many2many( 'mail.mass_mailing.contact', 'mail_mass_mailing_contact_list_rel', 'list_id', 'contact_id', string='Mailing Lists') subscription_contact_ids = fields.One2many('mail.mass_mailing.list_contact_rel', 'list_id', string='Subscription Information') is_public = fields.Boolean(default=True, help="The mailing list can be accessible by recipient in the unsubscription" " page to allows him to update his subscription preferences.") # Compute number of contacts non opt-out, non blacklisted and valid email recipient for a mailing list def _compute_contact_nbr(self): self.env.cr.execute(''' select list_id, count(*) from mail_mass_mailing_contact_list_rel r left join mail_mass_mailing_contact c on (r.contact_id=c.id) left join mail_blacklist bl on (LOWER(substring(c.email, %s)) = bl.email and bl.active) where list_id in %s AND COALESCE(r.opt_out,FALSE) = FALSE AND c.email IS NOT NULL AND bl.id IS NULL group by list_id ''', [EMAIL_PATTERN, tuple(self.ids)]) data = dict(self.env.cr.fetchall()) for mailing_list in self: mailing_list.contact_nbr = data.get(mailing_list.id, 0) @api.multi def name_get(self): return [(list.id, "%s (%s)" % (list.name, list.contact_nbr)) for list in self] @api.multi def action_merge(self, src_lists, archive): """ Insert all the contact from the mailing lists 'src_lists' to the mailing list in 'self'. Possibility to archive the mailing lists 'src_lists' after the merge except the destination mailing list 'self'. """ # Explation of the SQL query with an example. There are the following lists # A (id=4): [email protected]; [email protected] # B (id=5): [email protected]; [email protected] # C (id=6): nothing # To merge the mailing lists A and B into C, we build the view st that looks # like this with our example: # # contact_id | email | row_number | list_id | # ------------+---------------------------+------------------------ # 4 | [email protected] | 1 | 4 | # 6 | [email protected] | 2 | 5 | # 5 | [email protected] | 1 | 4 | # 7 | [email protected] | 1 | 5 | # # The row_column is kind of an occurence counter for the email address. # Then we create the Many2many relation between the destination list and the contacts # while avoiding to insert an existing email address (if the destination is in the source # for example) self.ensure_one() # Put destination is sources lists if not already the case src_lists |= self self.env.cr.execute(""" INSERT INTO mail_mass_mailing_contact_list_rel (contact_id, list_id) SELECT st.contact_id AS contact_id, %s AS list_id FROM ( SELECT contact.id AS contact_id, contact.email AS email, mailing_list.id AS list_id, row_number() OVER (PARTITION BY email ORDER BY email) AS rn FROM mail_mass_mailing_contact contact, mail_mass_mailing_contact_list_rel contact_list_rel, mail_mass_mailing_list mailing_list WHERE contact.id=contact_list_rel.contact_id AND COALESCE(contact_list_rel.opt_out,FALSE) = FALSE AND LOWER(substring(contact.email, %s)) NOT IN (select email from mail_blacklist where active = TRUE) AND mailing_list.id=contact_list_rel.list_id AND mailing_list.id IN %s AND NOT EXISTS ( SELECT 1 FROM mail_mass_mailing_contact contact2, mail_mass_mailing_contact_list_rel contact_list_rel2 WHERE contact2.email = contact.email AND contact_list_rel2.contact_id = contact2.id AND contact_list_rel2.list_id = %s ) ) st WHERE st.rn = 1;""", (self.id, EMAIL_PATTERN, tuple(src_lists.ids), self.id)) self.invalidate_cache() if archive: (src_lists - self).write({'active': False}) @api.multi def close_dialog(self): return {'type': 'ir.actions.act_window_close'}
class MassMailingCampaign(models.Model): """Model of mass mailing campaigns. """ _name = "mail.mass_mailing.campaign" _description = 'Mass Mailing Campaign' _rec_name = "campaign_id" _inherits = {'utm.campaign': 'campaign_id'} stage_id = fields.Many2one('mail.mass_mailing.stage', string='Stage', ondelete='restrict', required=True, default=lambda self: self.env['mail.mass_mailing.stage'].search([], limit=1), group_expand='_group_expand_stage_ids') user_id = fields.Many2one( 'res.users', string='Responsible', required=True, default=lambda self: self.env.uid) campaign_id = fields.Many2one('utm.campaign', 'campaign_id', required=True, ondelete='cascade', help="This name helps you tracking your different campaign efforts, e.g. Fall_Drive, Christmas_Special") source_id = fields.Many2one('utm.source', string='Source', help="This is the link source, e.g. Search Engine, another domain,or name of email list", default=lambda self: self.env.ref('utm.utm_source_newsletter', False)) medium_id = fields.Many2one('utm.medium', string='Medium', help="This is the delivery method, e.g. Postcard, Email, or Banner Ad", default=lambda self: self.env.ref('utm.utm_medium_email', False)) tag_ids = fields.Many2many( 'mail.mass_mailing.tag', 'mail_mass_mailing_tag_rel', 'tag_id', 'campaign_id', string='Tags') mass_mailing_ids = fields.One2many( 'mail.mass_mailing', 'mass_mailing_campaign_id', string='Mass Mailings') unique_ab_testing = fields.Boolean(string='Allow A/B Testing', default=False, help='If checked, recipients will be mailed only once for the whole campaign. ' 'This lets you send different mailings to randomly selected recipients and test ' 'the effectiveness of the mailings, without causing duplicate messages.') color = fields.Integer(string='Color Index') clicks_ratio = fields.Integer(compute="_compute_clicks_ratio", string="Number of clicks") # stat fields total = fields.Integer(compute="_compute_statistics") scheduled = fields.Integer(compute="_compute_statistics") failed = fields.Integer(compute="_compute_statistics") ignored = fields.Integer(compute="_compute_statistics") sent = fields.Integer(compute="_compute_statistics", string="Sent Emails") delivered = fields.Integer(compute="_compute_statistics") opened = fields.Integer(compute="_compute_statistics") replied = fields.Integer(compute="_compute_statistics") bounced = fields.Integer(compute="_compute_statistics") received_ratio = fields.Integer(compute="_compute_statistics", string='Received Ratio') opened_ratio = fields.Integer(compute="_compute_statistics", string='Opened Ratio') replied_ratio = fields.Integer(compute="_compute_statistics", string='Replied Ratio') bounced_ratio = fields.Integer(compute="_compute_statistics", string='Bounced Ratio') total_mailings = fields.Integer(compute="_compute_total_mailings", string='Mailings') def _compute_clicks_ratio(self): self.env.cr.execute(""" SELECT COUNT(DISTINCT(stats.id)) AS nb_mails, COUNT(DISTINCT(clicks.mail_stat_id)) AS nb_clicks, stats.mass_mailing_campaign_id AS id FROM mail_mail_statistics AS stats LEFT OUTER JOIN link_tracker_click AS clicks ON clicks.mail_stat_id = stats.id WHERE stats.mass_mailing_campaign_id IN %s GROUP BY stats.mass_mailing_campaign_id """, (tuple(self.ids), )) campaign_data = self.env.cr.dictfetchall() mapped_data = dict([(c['id'], 100 * c['nb_clicks'] / c['nb_mails']) for c in campaign_data]) for campaign in self: campaign.clicks_ratio = mapped_data.get(campaign.id, 0) def _compute_statistics(self): """ Compute statistics of the mass mailing campaign """ self.env.cr.execute(""" SELECT c.id as campaign_id, COUNT(s.id) AS total, COUNT(CASE WHEN s.sent is not null THEN 1 ELSE null END) AS sent, COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is null AND s.ignored is null THEN 1 ELSE null END) AS scheduled, COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is not null THEN 1 ELSE null END) AS failed, COUNT(CASE WHEN s.scheduled is not null AND s.sent is null AND s.exception is null AND s.ignored is not null THEN 1 ELSE null END) AS ignored, COUNT(CASE WHEN s.id is not null AND s.bounced is null THEN 1 ELSE null END) AS delivered, COUNT(CASE WHEN s.opened is not null THEN 1 ELSE null END) AS opened, COUNT(CASE WHEN s.replied is not null THEN 1 ELSE null END) AS replied , COUNT(CASE WHEN s.bounced is not null THEN 1 ELSE null END) AS bounced FROM mail_mail_statistics s RIGHT JOIN mail_mass_mailing_campaign c ON (c.id = s.mass_mailing_campaign_id) WHERE c.id IN %s GROUP BY c.id """, (tuple(self.ids), )) for row in self.env.cr.dictfetchall(): total = (row['total'] - row['ignored']) or 1 row['delivered'] = row['sent'] - row['bounced'] row['received_ratio'] = 100.0 * row['delivered'] / total row['opened_ratio'] = 100.0 * row['opened'] / total row['replied_ratio'] = 100.0 * row['replied'] / total row['bounced_ratio'] = 100.0 * row['bounced'] / total self.browse(row.pop('campaign_id')).update(row) def _compute_total_mailings(self): campaign_data = self.env['mail.mass_mailing'].read_group( [('mass_mailing_campaign_id', 'in', self.ids)], ['mass_mailing_campaign_id'], ['mass_mailing_campaign_id']) mapped_data = dict([(c['mass_mailing_campaign_id'][0], c['mass_mailing_campaign_id_count']) for c in campaign_data]) for campaign in self: campaign.total_mailings = mapped_data.get(campaign.id, 0) def get_recipients(self, model=None): """Return the recipients of a mailing campaign. This is based on the statistics build for each mailing. """ res = dict.fromkeys(self.ids, {}) for campaign in self: domain = [('mass_mailing_campaign_id', '=', campaign.id)] if model: domain += [('model', '=', model)] res[campaign.id] = set(self.env['mail.mail.statistics'].search(domain).mapped('res_id')) return res @api.model def _group_expand_stage_ids(self, stages, domain, order): """ Read group customization in order to display all the stages in the kanban view, even if they are empty """ stage_ids = stages._search([], order=order, access_rights_uid=SUPERUSER_ID) return stages.browse(stage_ids)
class MassMailingContact(models.Model): """Model of a contact. This model is different from the partner model because it holds only some basic information: name, email. The purpose is to be able to deal with large contact list to email without bloating the partner base.""" _name = 'mail.mass_mailing.contact' _inherit = ['mail.thread', 'mail.blacklist.mixin'] _description = 'Mass Mailing Contact' _order = 'email' _rec_name = 'email' name = fields.Char() company_name = fields.Char(string='Company Name') title_id = fields.Many2one('res.partner.title', string='Title') email = fields.Char(required=True) is_email_valid = fields.Boolean(compute='_compute_is_email_valid', store=True) list_ids = fields.Many2many( 'mail.mass_mailing.list', 'mail_mass_mailing_contact_list_rel', 'contact_id', 'list_id', string='Mailing Lists') subscription_list_ids = fields.One2many('mail.mass_mailing.list_contact_rel', 'contact_id', string='Subscription Information') message_bounce = fields.Integer(string='Bounced', help='Counter of the number of bounced emails for this contact.', default=0) country_id = fields.Many2one('res.country', string='Country') tag_ids = fields.Many2many('res.partner.category', string='Tags') opt_out = fields.Boolean('Opt Out', compute='_compute_opt_out', search='_search_opt_out', help='Opt out flag for a specific mailing list.' 'This field should not be used in a view without a unique and active mailing list context.') @api.depends('email') def _compute_is_email_valid(self): for record in self: record.is_email_valid = re.match(EMAIL_PATTERN, record.email) @api.model def _search_opt_out(self, operator, value): # Assumes operator is '=' or '!=' and value is True or False if operator != '=': if operator == '!=' and isinstance(value, bool): value = not value else: raise NotImplementedError() if 'default_list_ids' in self._context and isinstance(self._context['default_list_ids'], (list, tuple)) and len(self._context['default_list_ids']) == 1: [active_list_id] = self._context['default_list_ids'] contacts = self.env['mail.mass_mailing.list_contact_rel'].search([('list_id', '=', active_list_id)]) return [('id', 'in', [record.contact_id.id for record in contacts if record.opt_out == value])] else: raise UserError('Search opt out cannot be executed without a unique and valid active mailing list context.') @api.depends('subscription_list_ids') def _compute_opt_out(self): if 'default_list_ids' in self._context and isinstance(self._context['default_list_ids'], (list, tuple)) and len(self._context['default_list_ids']) == 1: [active_list_id] = self._context['default_list_ids'] for record in self: active_subscription_list = record.subscription_list_ids.filtered(lambda l: l.list_id.id == active_list_id) record.opt_out = active_subscription_list.opt_out else: for record in self: record.opt_out = False def get_name_email(self, name): name, email = self.env['res.partner']._parse_partner_name(name) if name and not email: email = name if email and not name: name = email return name, email @api.model def name_create(self, name): name, email = self.get_name_email(name) contact = self.create({'name': name, 'email': email}) return contact.name_get()[0] @api.model def add_to_list(self, name, list_id): name, email = self.get_name_email(name) contact = self.create({'name': name, 'email': email, 'list_ids': [(4, list_id)]}) return contact.name_get()[0] @api.multi def message_get_default_recipients(self): return dict((record.id, {'partner_ids': [], 'email_to': record.email, 'email_cc': False}) for record in self)
class GradeMaster(models.Model): _name = 'grade.master' _description = "Grade Master" name = fields.Char('Grade', required=True) grade_ids = fields.One2many('grade.line', 'grade_id', 'Grade Lines')
class MrpProduction(models.Model): _inherit = 'mrp.production' move_raw_ids = fields.One2many( 'stock.move', 'raw_material_production_id', 'Raw Materials', oldname='move_lines', copy=False, states={ 'done': [('readonly', True)], 'cancel': [('readonly', True)] }, domain=[('scrapped', '=', False), ('state', '!=', 'cancel')], ) @api.multi def _generate_moves(self): res = super(MrpProduction, self)._generate_moves() for production in self: if production.move_raw_ids: for move in production.move_raw_ids: # CONTENDRA VALOR ORIGINAL A CONSUMIR (INFORMATIVO) move.product_uom_qty_original = move.product_uom_qty if move.bom_line_id and production.product_id.categ_id.mrp_bom_modification: move.real_p = move.bom_line_id.bom_p return res def get_min_qty(self, move, production_qty): """OBTIENE EL % CORRESPONDIENTE A LA CANTIDAD ESTABLECIDA EN LA LISTA DE MATERIAL.""" bom_qty = move.bom_line_id.product_qty formula_p = round(move.formula_p, 5) min_qty = ((bom_qty * formula_p) / 100) * production_qty return min_qty def check_move_line(self, move): if move.obligatorio: if move.bom_line_id and not move.new_bom_line and move.raw_material_production_id: production_qty = move.raw_material_production_id.product_qty # 100% product_uom_qty = move.product_uom_qty # ? min_qty = self.get_min_qty(move, production_qty) if product_uom_qty < min_qty: raise ValidationError( _('La cantidad minima permitida para ' + move.product_id.name + ' es ' + str(min_qty))) return @api.multi def check_percentage(self): """REVISA QUE SE CUMPLA LA CONDICION DE 100% DE LISTA DE MATERIALES.""" for production in self: if production.product_id.categ_id and production.product_id.categ_id.mrp_bom_modification: # SE OBTIENE EL 100% DE LA LISTA DE MATERIALES ORIGINAL (SUMA DEL TOTAL DE CANTIDADES) bom_total = 0 if production.bom_id and production.bom_id.bom_line_ids: bom_total = sum([ ((line.product_qty / production.bom_id.product_qty) * production.product_qty) for line in production.bom_id.bom_line_ids ]) # SE OBTIENE EL 100% DE LOS MATERIALES A CONSUMIR production_total = 0 if production.move_raw_ids: for move in production.move_raw_ids: self.check_move_line(move) production_total = sum([ move.product_uom_qty for move in production.move_raw_ids if move.state not in ('cancel') ]) differencia_perc = bom_total / production_total if differencia_perc - 1 > _ALLOWED_DIFFERENCE_PERC: str_differencia = str('{0:f}'.format( (1 - differencia_perc) * 100)) raise ValidationError( _('No alcanza el 100% de cantidad de produccion necesario' + '\nTotal Lista de materiales = ' + str(bom_total) + '\nTotal Materiales a consumir = ' + str(production_total) + '\nDiferencia porcentual: ' + str(str_differencia))) # HEREDA METODOS EXISTENTES Y AGREGA CHECKEO DE PORCENTAGES @api.multi def button_plan(self): if self.state not in ('done', 'cancel', 'progress'): self.check_percentage() super(MrpProduction, self).button_plan() @api.multi def action_assign(self): if self.state not in ('done', 'cancel', 'progress'): self.check_percentage() super(MrpProduction, self).action_assign()
class Currency(models.Model): _name = "res.currency" _description = "Currency" _order = 'active desc, name' # Note: 'code' column was removed as of v6.0, the 'name' should now hold the ISO code. name = fields.Char(string='Currency', size=3, required=True, help="Currency Code (ISO 4217)") symbol = fields.Char(help="Currency sign, to be used when printing amounts.", required=True) rate = fields.Float(compute='_compute_current_rate', string='Current Rate', digits=(12, 6), help='The rate of the currency to the currency of rate 1.') rate_ids = fields.One2many('res.currency.rate', 'currency_id', string='Rates') rounding = fields.Float(string='Rounding Factor', digits=(12, 6), default=0.01) decimal_places = fields.Integer(compute='_compute_decimal_places') active = fields.Boolean(default=True) position = fields.Selection([('after', 'After Amount'), ('before', 'Before Amount')], default='after', string='Symbol Position', help="Determines where the currency symbol should be placed after or before the amount.") date = fields.Date(compute='_compute_date') currency_unit_label = fields.Char(string="Currency Unit", help="Currency Unit Name") currency_subunit_label = fields.Char(string="Currency Subunit", help="Currency Subunit Name") _sql_constraints = [ ('unique_name', 'unique (name)', 'The currency code must be unique!'), ('rounding_gt_zero', 'CHECK (rounding>0)', 'The rounding factor must be greater than 0!') ] def _get_rates(self, company, date): query = """SELECT c.id, (SELECT r.rate FROM res_currency_rate r WHERE r.currency_id = c.id AND r.name <= %s AND (r.company_id IS NULL OR r.company_id = %s) ORDER BY r.company_id, r.name DESC LIMIT 1) AS rate FROM res_currency c WHERE c.id IN %s""" self._cr.execute(query, (date, company.id, tuple(self.ids))) currency_rates = dict(self._cr.fetchall()) return currency_rates @api.multi def _compute_current_rate(self): date = self._context.get('date') or fields.Date.today() company = self._context.get('company_id') or self.env['res.users']._get_company() # the subquery selects the last rate before 'date' for the given currency/company currency_rates = self._get_rates(company, date) for currency in self: currency.rate = currency_rates.get(currency.id) or 1.0 @api.multi @api.depends('rounding') def _compute_decimal_places(self): for currency in self: if 0 < currency.rounding < 1: currency.decimal_places = int(math.ceil(math.log10(1/currency.rounding))) else: currency.decimal_places = 0 @api.multi @api.depends('rate_ids.name') def _compute_date(self): for currency in self: currency.date = currency.rate_ids[:1].name @api.model def _name_search(self, name, args=None, operator='ilike', limit=100, name_get_uid=None): results = super(Currency, self)._name_search(name, args, operator=operator, limit=limit, name_get_uid=name_get_uid) if not results: name_match = CURRENCY_DISPLAY_PATTERN.match(name) if name_match: results = super(Currency, self)._name_search(name_match.group(1), args, operator=operator, limit=limit, name_get_uid=name_get_uid) return results @api.multi def name_get(self): return [(currency.id, tools.ustr(currency.name)) for currency in self] @api.multi def amount_to_text(self, amount): self.ensure_one() def _num2words(number, lang): try: return num2words(number, lang=lang).title() except NotImplementedError: return num2words(number, lang='en').title() if num2words is None: logging.getLogger(__name__).warning("The library 'num2words' is missing, cannot render textual amounts.") return "" formatted = "%.{0}f".format(self.decimal_places) % amount parts = formatted.partition('.') integer_value = int(parts[0]) fractional_value = int(parts[2] or 0) lang_code = self.env.context.get('lang') or self.env.user.lang lang = self.env['res.lang'].search([('code', '=', lang_code)]) amount_words = tools.ustr('{amt_value} {amt_word}').format( amt_value=_num2words(integer_value, lang=lang.iso_code), amt_word=self.currency_unit_label, ) if not self.is_zero(amount - integer_value): amount_words += ' ' + _('and') + tools.ustr(' {amt_value} {amt_word}').format( amt_value=_num2words(fractional_value, lang=lang.iso_code), amt_word=self.currency_subunit_label, ) return amount_words @api.multi def round(self, amount): """Return ``amount`` rounded according to ``self``'s rounding rules. :param float amount: the amount to round :return: rounded float """ # TODO: Need to check why it calls round() from sale.py, _amount_all() with *No* ID after below commits, # https://github.com/odoo/odoo/commit/36ee1ad813204dcb91e9f5f20d746dff6f080ac2 # https://github.com/odoo/odoo/commit/0b6058c585d7d9a57bd7581b8211f20fca3ec3f7 # Removing self.ensure_one() will make few test cases to break of modules event_sale, sale_mrp and stock_dropshipping. #self.ensure_one() return tools.float_round(amount, precision_rounding=self.rounding) @api.multi def compare_amounts(self, amount1, amount2): """Compare ``amount1`` and ``amount2`` after rounding them according to the given currency's precision.. An amount is considered lower/greater than another amount if their rounded value is different. This is not the same as having a non-zero difference! For example 1.432 and 1.431 are equal at 2 digits precision, so this method would return 0. However 0.006 and 0.002 are considered different (returns 1) because they respectively round to 0.01 and 0.0, even though 0.006-0.002 = 0.004 which would be considered zero at 2 digits precision. :param float amount1: first amount to compare :param float amount2: second amount to compare :return: (resp.) -1, 0 or 1, if ``amount1`` is (resp.) lower than, equal to, or greater than ``amount2``, according to ``currency``'s rounding. With the new API, call it like: ``currency.compare_amounts(amount1, amount2)``. """ return tools.float_compare(amount1, amount2, precision_rounding=self.rounding) @api.multi def is_zero(self, amount): """Returns true if ``amount`` is small enough to be treated as zero according to current currency's rounding rules. Warning: ``is_zero(amount1-amount2)`` is not always equivalent to ``compare_amounts(amount1,amount2) == 0``, as the former will round after computing the difference, while the latter will round before, giving different results for e.g. 0.006 and 0.002 at 2 digits precision. :param float amount: amount to compare with currency's zero With the new API, call it like: ``currency.is_zero(amount)``. """ return tools.float_is_zero(amount, precision_rounding=self.rounding) @api.model def _get_conversion_rate(self, from_currency, to_currency, company, date): currency_rates = (from_currency + to_currency)._get_rates(company, date) res = currency_rates.get(to_currency.id) / currency_rates.get(from_currency.id) return res def _convert(self, from_amount, to_currency, company, date, round=True): """Returns the converted amount of ``from_amount``` from the currency ``self`` to the currency ``to_currency`` for the given ``date`` and company. :param company: The company from which we retrieve the convertion rate :param date: The nearest date from which we retriev the conversion rate. :param round: Round the result or not """ self, to_currency = self or to_currency, to_currency or self assert self, "convert amount from unknown currency" assert to_currency, "convert amount to unknown currency" assert company, "convert amount from unknown company" assert date, "convert amount from unknown date" # apply conversion rate if self == to_currency: to_amount = from_amount else: to_amount = from_amount * self._get_conversion_rate(self, to_currency, company, date) # apply rounding return to_currency.round(to_amount) if round else to_amount @api.model def _compute(self, from_currency, to_currency, from_amount, round=True): _logger.warning('The `_compute` method is deprecated. Use `_convert` instead') date = self._context.get('date') or fields.Date.today() company = self.env['res.company'].browse(self._context.get('company_id')) or self.env['res.users']._get_company() return from_currency._convert(from_amount, to_currency, company, date) @api.multi def compute(self, from_amount, to_currency, round=True): _logger.warning('The `compute` method is deprecated. Use `_convert` instead') date = self._context.get('date') or fields.Date.today() company = self.env['res.company'].browse(self._context.get('company_id')) or self.env['res.users']._get_company() return self._convert(from_amount, to_currency, company, date) def _select_companies_rates(self): return """
class LabelPrint(models.Model): _name = "label.print" name = fields.Char("Name", size=64, required=True, index=True) model_id = fields.Many2one('ir.model', 'Model', required=True, index=True) mode = fields.Selection([ ('fields', u"Définir les champs"), ('template', u"Utiliser un template"), ], string=u"Mode de définition", default='template') template_id = fields.Many2one(comodel_name="ir.ui.view", help=u""" template Qweb définissant l'affichage de l'intérieur de l'étiquette ATTENTION: ce template doit avoir un ID externe pour être pris en compte. s'il n'en a pas (création à la volée) vous pouvez en générer un en exportant votre template le sélectionner en vue liste et faire action > exporter """) template_id_arch = fields.Text(related="template_id.arch_base", readonly=True) template_css_id = fields.Many2one(comodel_name="ir.ui.view", help=u""" template Qweb définissant la feuille de style de l'intérieur de l'étiquette ATTENTION: ce template doit avoir un ID externe pour être pris en compte. s'il n'en a pas (création à la volée) vous pouvez en générer un en exportant votre template le sélectionner en vue liste et faire action > exporter """) template_css_id_arch = fields.Text(related="template_css_id.arch_base", readonly=True) field_ids = fields.One2many("label.print.field", 'report_id', string='Fields', copy=True) ref_ir_act_report = fields.Many2one('ir.actions.act_window', 'Sidebar action', readonly=True, help="""Sidebar action to make this template available on records of the related document model""") ref_ir_value = fields.Many2one('ir.values', 'Sidebar button', readonly=True, help="Sidebar button to open the \ sidebar action") model_list = fields.Char('Model List', size=256) active = fields.Boolean(string=u"Active", default=True) label_brand_id = fields.Many2one(comodel_name='label.brand', string='Brand', required=True) label_config_id = fields.Many2one(comodel_name='label.config', string='Template', required=True) line_field_id = fields.Many2one( comodel_name='ir.model.fields', string=u"Champ des lignes", domain="[('id', 'in', line_field_domain and line_field_domain[0] and line_field_domain[0][2] or False)]") line_field_domain = fields.Many2many(comodel_name='ir.model.fields', compute='_compute_line_field_domain') line_field_name = fields.Char(string=u"Nom du champ de lignes", compute="_compute_line_model_id", store=True) line_model_id = fields.Many2one( comodel_name='ir.model', string=u"Modèle des lignes", compute="_compute_line_model_id", store=True) @api.depends('model_id') def _compute_line_field_domain(self): for record in self: field_ids_list = [] if record.model_id: # récupérer les id de tous les champs O2M ou M2M de record.model_id fields = self.env['ir.model.fields'].search( [('model', '=', record.model_id.model), ('ttype', 'in', ['one2many', 'many2many'])]) field_ids_list = fields and fields.ids or [] record.line_field_domain = field_ids_list @api.depends('line_field_id') def _compute_line_model_id(self): for record in self: if record.line_field_id: model_name = record.line_field_id.relation model_id = self.env['ir.model'].search([('model', '=', model_name)]) record.line_model_id = model_id and model_id[0] or False record.line_field_name = record.line_field_id.name @api.onchange('model_id', 'line_field_id') def onchange_models_id(self): self.ensure_one() model_id = self.line_model_id or self.model_id or False model_list = [] if model_id: model_obj = self.env['ir.model'] current_model = model_id.model model_list.append(current_model) active_model_obj = self.env[model_id.model] if active_model_obj._inherits: for key, val in active_model_obj._inherits.items(): model_ids = model_obj.search([('model', '=', key)]) if model_ids: model_list.append(key) field_code = [] for field in self.field_ids: if isinstance(field.id, int) and field.field_id.model_id != model_id: field_code.append((2, field.id, 0)) self.field_ids = field_code self.model_list = model_list @api.onchange('model_id') def onchange_model_id(self): u"""Mettre à jour le domain des champs de lignes possibles""" field_ids_list = [] if self.model_id: # récupérer les id de tous les champs O2M ou M2M de record.model_id fields = self.env['ir.model.fields'].search( [('model', '=', self.model_id.model), ('ttype', 'in', ['one2many', 'many2many'])]) field_ids_list = fields and fields.ids or [] self.line_field_domain = field_ids_list if self.line_field_id and self.line_field_id.id not in field_ids_list: self.line_field_id = False res = {'domain': {'line_field_id': [('id', 'in', field_ids_list)]}} return res @api.multi def create_action(self): vals = {} action_obj = self.env['ir.actions.act_window'] for data in self.browse(self.ids): src_obj = data.model_id.model button_name = _('Label (%s)') % data.name vals['ref_ir_act_report'] = action_obj.create({ 'name': button_name, 'type': 'ir.actions.act_window', 'res_model': 'label.print.wizard', 'src_model': src_obj, 'view_type': 'form', 'context': "{'label_print' : %d}" % (data.id), 'view_mode': 'form,tree', 'target': 'new', }) id_temp = vals['ref_ir_act_report'].id vals['ref_ir_value'] = self.env['ir.values'].create({ 'name': button_name, 'model': src_obj, 'key2': 'client_action_multi', 'value': "ir.actions.act_window," + str(id_temp), 'object': True, }) self.write({ 'ref_ir_act_report': vals.get('ref_ir_act_report', False).id, 'ref_ir_value': vals.get('ref_ir_value', False).id, }) return True @api.multi def unlink_action(self): for template in self: if template.ref_ir_act_report.id: template.ref_ir_act_report.unlink() if template.ref_ir_value.id: template.ref_ir_value.unlink() return True
class ImportLettersHistory(models.Model): """ Keep history of imported letters. This class allows the user to import some letters (individually or in a zip file) in the database by doing an automatic analysis. The code is reading QR codes in order to detect child and partner codes for every letter, using the zxing library for code detection. """ _name = "import.letters.history" _inherit = ["import.letter.config", "mail.thread"] _description = _("""History of the letters imported from a zip or a PDF/TIFF""") _order = "create_date desc" _rec_name = "create_date" ########################################################################## # FIELDS # ########################################################################## state = fields.Selection( [ ("draft", _("Draft")), ("pending", _("Analyzing")), ("open", _("Open")), ("ready", _("Ready")), ("done", _("Done")), ], compute="_compute_state", store=True, track_visibility="onchange", ) import_completed = fields.Boolean() nber_letters = fields.Integer("Number of files", readonly=True, compute="_compute_nber_letters") data = fields.Many2many("ir.attachment", string="Add a file", readonly=False) import_line_ids = fields.One2many( "import.letter.line", "import_id", "Files to process", ondelete="cascade", readonly=False, ) letters_ids = fields.One2many("correspondence", "import_id", "Imported letters", readonly=True) config_id = fields.Many2one("import.letter.config", "Import settings", readonly=False) ########################################################################## # FIELDS METHODS # ########################################################################## @api.multi @api.depends( "import_line_ids", "import_line_ids.status", "letters_ids", "data", "import_completed", ) def _compute_state(self): """ Check in which state self is by counting the number of elements in each Many2many """ for import_letters in self: if import_letters.letters_ids: import_letters.state = "done" elif import_letters.import_completed: check = True for i in import_letters.import_line_ids: if i.status != "ok": check = False if check: import_letters.state = "ready" else: import_letters.state = "open" elif import_letters.import_line_ids: import_letters.state = "pending" else: import_letters.state = "draft" @api.onchange("data") def _compute_nber_letters(self): """ Counts the number of scans. If a zip file is given, the number of scans inside is counted. """ for letter in self: if letter.state in ("open", "pending", "ready"): letter.nber_letters = len(letter.import_line_ids) elif letter.state == "done": letter.nber_letters = len(letter.letters_ids) elif letter.state is False or letter.state == "draft": # counter tmp = 0 # loop over all the attachments for attachment in letter.data: # pdf or tiff case if func.check_file(attachment.name) == 1: tmp += 1 # zip case elif func.is_zip(attachment.name): # create a tempfile and read it zip_file = BytesIO( base64.b64decode( attachment.with_context(bin_size=False).datas)) # catch ALL the exceptions that can be raised # by class zipfile try: zip_ = zipfile.ZipFile(zip_file, "r") list_file = zip_.namelist() # loop over all files in zip for tmp_file in list_file: tmp += func.check_file(tmp_file) == 1 except zipfile.BadZipfile: raise UserError( _("Zip file corrupted (" + attachment.name + ")")) except zipfile.LargeZipFile: raise UserError( _("Zip64 is not supported(" + attachment.name + ")")) letter.nber_letters = tmp else: raise UserError( _("State: '%s' not implemented") % letter.state) ########################################################################## # ORM METHODS # ########################################################################## @api.model def create(self, vals): if vals.get("config_id"): other_import = self.search_count([("config_id", "=", vals["config_id"]), ("state", "!=", "done")]) if other_import: raise UserError( _("Another import with the same configuration is " "already open. Please finish it before creating a new " "one.")) return super().create(vals) ########################################################################## # VIEW CALLBACKS # ########################################################################## @api.multi def button_import(self): """ Analyze the attachment in order to create the letter's lines """ for letters_import in self: if letters_import.data: letters_import.state = "pending" if self.env.context.get("async_mode", True): letters_import.with_delay()._run_analyze() else: letters_import._run_analyze() return True @api.multi def button_save(self): """ save the import_line as a correspondence """ # check if all the imports are OK for letters_h in self: if letters_h.state != "ready": raise UserError(_("Some letters are not ready")) # save the imports for letters in self: correspondence_vals = letters.import_line_ids.get_letter_data() # letters_ids should be empty before this line for vals in correspondence_vals: letters.letters_ids.create(vals) letters.import_line_ids.unlink() return True @api.multi def button_review(self): """ Returns a form view for import lines in order to browse them """ self.ensure_one() return { "name": _("Review Imports"), "type": "ir.actions.act_window", "view_type": "form", "view_mode": "form", "res_model": "import.letters.review", "context": self.with_context(line_ids=self.import_line_ids.ids).env.context, "target": "current", } @api.onchange("config_id") def onchange_config(self): config = self.config_id if config: for field, val in list( config.get_correspondence_metadata().items()): setattr(self, field, val) ########################################################################## # PRIVATE METHODS # ########################################################################## @job(default_channel="root.sbc_compassion") @related_action(action="related_action_s2b_imports") def _run_analyze(self): """ Analyze each attachment: - check for duplicate file names and skip them - decompress zip file if necessary - call _analyze_attachment for every resulting file """ self.ensure_one() # keep track of file names to detect duplicates file_name_history = [] logger.info("Imported files analysis started...") progress = 1 for attachment in self.data: if attachment.name not in file_name_history: file_name_history.append(attachment.name) file_data = base64.b64decode( attachment.with_context(bin_size=False).datas) # check for zip if func.check_file(attachment.name) == 2: zip_file = BytesIO(file_data) zip_ = zipfile.ZipFile(zip_file, "r") for f in zip_.namelist(): logger.debug( f"Analyzing file {progress}/{self.nber_letters}") self._analyze_attachment(zip_.read(f), f) progress += 1 # case with normal format (PDF,TIFF) elif func.check_file(attachment.name) == 1: logger.debug( f"Analyzing file {progress}/{self.nber_letters}") self._analyze_attachment(file_data, attachment.name) progress += 1 else: raise UserError(_("Only zip/pdf files are supported.")) else: raise UserError(_("Two files are the same")) # remove all the files (now they are inside import_line_ids) self.data.unlink() self.import_completed = True logger.info("Imported files analysis completed.") def _analyze_attachment(self, file_data, file_name): line_vals = func.analyze_attachment(self.env, file_data, file_name, self.template_id) for i in range(0, len(line_vals)): line_vals[i]["import_id"] = self.id self.env["import.letter.line"].create(line_vals[i])
class HousingModel(models.Model): _name = 'housing.model' _inherit = ['mail.thread', 'mail.activity.mixin'] _check_company_auto = True active = fields.Boolean(default=True) name = fields.Char(string="Model", required=True, track_visibility="always") floor_area = fields.Float(string="Floor Area", track_visibility="always") lot_area = fields.Float(string="Lot Area", track_visibility="always") lot_area_price = fields.Monetary(string="Lot Area Price", help="Lot Area Price") floor_area_price = fields.Monetary(string="Floor Area Price", help="Floor Area Price", track_visibility="always") miscellaneous_value = fields.Monetary(string="MCC2", default=9, help="Miscellaneous Absolute Value") miscellaneous_charge = fields.Float(string="MCC", default=9, help="Miscellaneous Charge (%)") reservation_fee = fields.Monetary(string="Reservation Fee") downpayment_percent = fields.Float(string="Downpayment", default="15", help="Downpayment Term (%)") # downpayment_term = fields.Integer(string="DP Term", default="12", help="Downpayment Term") property_type = fields.Selection([('House', 'House and Lot'), ('Condo', 'Condo Unit')], string="Property Type", default="House", required=True) model_type_id = fields.Many2one("property.model.type", string="Model Type", track_visibility="always") description = fields.Text(string="Description", track_visibility="always") year_month = fields.Char(string="Year Month", track_visibility="always") model_blue_print = fields.Binary(string="Plan", track_visibility="always") house_model_image_ids = fields.One2many('product.image', 'housing_model_tmpl_id', string='Images') brand_id = fields.Many2one('product.brand', string='Brand') company_id = fields.Many2one('res.company', 'Company', required=True, index=True, default=lambda self: self.env.company) currency_id = fields.Many2one('res.currency', string="Currency", related="company_id.currency_id", check_company=True) def unlink(self): for rec in self: property = rec.env['property.detail'].search([('house_model_id', '=', rec.id)]).ids property += rec.env[ 'property.subdivision.phase.unit.model'].search([ ('house_model_id', '=', rec.id) ]).ids if property: raise ValidationError( _('Delete first the property associated to this Subdivision Phase' )) return super(HousingModel, self).unlink()
class ProductProduct(models.Model): _name = "product.product" _description = "Product" _inherits = {'product.template': 'product_tmpl_id'} _inherit = ['mail.thread', 'mail.activity.mixin'] _order = 'default_code, name, id' # price: total price, context dependent (partner, pricelist, quantity) price = fields.Float( 'Price', compute='_compute_product_price', digits='Product Price', inverse='_set_product_price') # price_extra: catalog extra value only, sum of variant extra attributes price_extra = fields.Float( 'Variant Price Extra', compute='_compute_product_price_extra', digits='Product Price', help="This is the sum of the extra price of all attributes") # lst_price: catalog value + extra, context dependent (uom) lst_price = fields.Float( 'Public Price', compute='_compute_product_lst_price', digits='Product Price', inverse='_set_product_lst_price', help="The sale price is managed from the product template. Click on the 'Configure Variants' button to set the extra attribute prices.") default_code = fields.Char('Internal Reference', index=True) code = fields.Char('Reference', compute='_compute_product_code') partner_ref = fields.Char('Customer Ref', compute='_compute_partner_ref') active = fields.Boolean( 'Active', default=True, help="If unchecked, it will allow you to hide the product without removing it.") product_tmpl_id = fields.Many2one( 'product.template', 'Product Template', auto_join=True, index=True, ondelete="cascade", required=True) barcode = fields.Char( 'Barcode', copy=False, help="International Article Number used for product identification.") product_template_attribute_value_ids = fields.Many2many('product.template.attribute.value', relation='product_variant_combination', string="Attribute Values", ondelete='restrict') combination_indices = fields.Char(compute='_compute_combination_indices', store=True, index=True) is_product_variant = fields.Boolean(compute='_compute_is_product_variant') standard_price = fields.Float( 'Cost', company_dependent=True, digits='Product Price', groups="base.group_user", help="""In Standard Price & AVCO: value of the product (automatically computed in AVCO). In FIFO: value of the last unit that left the stock (automatically computed). Used to value the product when the purchase cost is not known (e.g. inventory adjustment). Used to compute margins on sale orders.""") volume = fields.Float('Volume', digits='Volume') weight = fields.Float('Weight', digits='Stock Weight') pricelist_item_count = fields.Integer("Number of price rules", compute="_compute_variant_item_count") packaging_ids = fields.One2many( 'product.packaging', 'product_id', 'Product Packages', help="Gives the different ways to package the same product.") # all image fields are base64 encoded and PIL-supported # all image_variant fields are technical and should not be displayed to the user image_variant_1920 = fields.Image("Variant Image", max_width=1920, max_height=1920) # resized fields stored (as attachment) for performance image_variant_1024 = fields.Image("Variant Image 1024", related="image_variant_1920", max_width=1024, max_height=1024, store=True) image_variant_512 = fields.Image("Variant Image 512", related="image_variant_1920", max_width=512, max_height=512, store=True) image_variant_256 = fields.Image("Variant Image 256", related="image_variant_1920", max_width=256, max_height=256, store=True) image_variant_128 = fields.Image("Variant Image 128", related="image_variant_1920", max_width=128, max_height=128, store=True) can_image_variant_1024_be_zoomed = fields.Boolean("Can Variant Image 1024 be zoomed", compute='_compute_can_image_variant_1024_be_zoomed', store=True) # Computed fields that are used to create a fallback to the template if # necessary, it's recommended to display those fields to the user. image_1920 = fields.Image("Image", compute='_compute_image_1920', inverse='_set_image_1920') image_1024 = fields.Image("Image 1024", compute='_compute_image_1024') image_512 = fields.Image("Image 512", compute='_compute_image_512') image_256 = fields.Image("Image 256", compute='_compute_image_256') image_128 = fields.Image("Image 128", compute='_compute_image_128') can_image_1024_be_zoomed = fields.Boolean("Can Image 1024 be zoomed", compute='_compute_can_image_1024_be_zoomed') @api.depends('image_variant_1920', 'image_variant_1024') def _compute_can_image_variant_1024_be_zoomed(self): for record in self: record.can_image_variant_1024_be_zoomed = record.image_variant_1920 and tools.is_image_size_above(record.image_variant_1920, record.image_variant_1024) def _compute_image_1920(self): """Get the image from the template if no image is set on the variant.""" for record in self: record.image_1920 = record.image_variant_1920 or record.product_tmpl_id.image_1920 def _set_image_1920(self): for record in self: if ( # We are trying to remove an image even though it is already # not set, remove it from the template instead. not record.image_1920 and not record.image_variant_1920 or # We are trying to add an image, but the template image is # not set, write on the template instead. record.image_1920 and not record.product_tmpl_id.image_1920 or # There is only one variant, always write on the template. self.search_count([ ('product_tmpl_id', '=', record.product_tmpl_id.id), ('active', '=', True), ]) <= 1 ): record.image_variant_1920 = False record.product_tmpl_id.image_1920 = record.image_1920 else: record.image_variant_1920 = record.image_1920 def _compute_image_1024(self): """Get the image from the template if no image is set on the variant.""" for record in self: record.image_1024 = record.image_variant_1024 or record.product_tmpl_id.image_1024 def _compute_image_512(self): """Get the image from the template if no image is set on the variant.""" for record in self: record.image_512 = record.image_variant_512 or record.product_tmpl_id.image_512 def _compute_image_256(self): """Get the image from the template if no image is set on the variant.""" for record in self: record.image_256 = record.image_variant_256 or record.product_tmpl_id.image_256 def _compute_image_128(self): """Get the image from the template if no image is set on the variant.""" for record in self: record.image_128 = record.image_variant_128 or record.product_tmpl_id.image_128 def _compute_can_image_1024_be_zoomed(self): """Get the image from the template if no image is set on the variant.""" for record in self: record.can_image_1024_be_zoomed = record.can_image_variant_1024_be_zoomed if record.image_variant_1920 else record.product_tmpl_id.can_image_1024_be_zoomed def init(self): """Ensure there is at most one active variant for each combination. There could be no variant for a combination if using dynamic attributes. """ self.env.cr.execute("CREATE UNIQUE INDEX IF NOT EXISTS product_product_combination_unique ON %s (product_tmpl_id, combination_indices) WHERE active is true" % self._table) _sql_constraints = [ ('barcode_uniq', 'unique(barcode)', "A barcode can only be assigned to one product !"), ] def _get_invoice_policy(self): return False @api.depends('product_template_attribute_value_ids') def _compute_combination_indices(self): for product in self: product.combination_indices = product.product_template_attribute_value_ids._ids2str() def _compute_is_product_variant(self): for product in self: product.is_product_variant = True @api.depends_context('pricelist', 'partner', 'quantity', 'uom', 'date', 'no_variant_attributes_price_extra') def _compute_product_price(self): prices = {} pricelist_id_or_name = self._context.get('pricelist') if pricelist_id_or_name: pricelist = None partner = self.env.context.get('partner', False) quantity = self.env.context.get('quantity', 1.0) # Support context pricelists specified as list, display_name or ID for compatibility if isinstance(pricelist_id_or_name, list): pricelist_id_or_name = pricelist_id_or_name[0] if isinstance(pricelist_id_or_name, str): pricelist_name_search = self.env['product.pricelist'].name_search(pricelist_id_or_name, operator='=', limit=1) if pricelist_name_search: pricelist = self.env['product.pricelist'].browse([pricelist_name_search[0][0]]) elif isinstance(pricelist_id_or_name, int): pricelist = self.env['product.pricelist'].browse(pricelist_id_or_name) if pricelist: quantities = [quantity] * len(self) partners = [partner] * len(self) prices = pricelist.get_products_price(self, quantities, partners) for product in self: product.price = prices.get(product.id, 0.0) def _set_product_price(self): for product in self: if self._context.get('uom'): value = self.env['uom.uom'].browse(self._context['uom'])._compute_price(product.price, product.uom_id) else: value = product.price value -= product.price_extra product.write({'list_price': value}) def _set_product_lst_price(self): for product in self: if self._context.get('uom'): value = self.env['uom.uom'].browse(self._context['uom'])._compute_price(product.lst_price, product.uom_id) else: value = product.lst_price value -= product.price_extra product.write({'list_price': value}) def _compute_product_price_extra(self): for product in self: product.price_extra = sum(product.product_template_attribute_value_ids.mapped('price_extra')) @api.depends('list_price', 'price_extra') @api.depends_context('uom') def _compute_product_lst_price(self): to_uom = None if 'uom' in self._context: to_uom = self.env['uom.uom'].browse(self._context['uom']) for product in self: if to_uom: list_price = product.uom_id._compute_price(product.list_price, to_uom) else: list_price = product.list_price product.lst_price = list_price + product.price_extra @api.depends_context('partner_id') def _compute_product_code(self): for product in self: for supplier_info in product.seller_ids: if supplier_info.name.id == product._context.get('partner_id'): product.code = supplier_info.product_code or product.default_code break else: product.code = product.default_code @api.depends_context('partner_id') def _compute_partner_ref(self): for product in self: for supplier_info in product.seller_ids: if supplier_info.name.id == product._context.get('partner_id'): product_name = supplier_info.product_name or product.default_code or product.name product.partner_ref = '%s%s' % (product.code and '[%s] ' % product.code or '', product_name) break else: product.partner_ref = product.display_name def _compute_variant_item_count(self): for product in self: domain = ['|', '&', ('product_tmpl_id', '=', product.product_tmpl_id.id), ('applied_on', '=', '1_product'), '&', ('product_id', '=', product.id), ('applied_on', '=', '0_product_variant')] product.pricelist_item_count = self.env['product.pricelist.item'].search_count(domain) @api.onchange('uom_id', 'uom_po_id') def _onchange_uom(self): if self.uom_id and self.uom_po_id and self.uom_id.category_id != self.uom_po_id.category_id: self.uom_po_id = self.uom_id @api.model_create_multi def create(self, vals_list): products = super(ProductProduct, self.with_context(create_product_product=True)).create(vals_list) # `_get_variant_id_for_combination` depends on existing variants self.clear_caches() return products def write(self, values): res = super(ProductProduct, self).write(values) if 'product_template_attribute_value_ids' in values: # `_get_variant_id_for_combination` depends on `product_template_attribute_value_ids` self.clear_caches() if 'active' in values: # prefetched o2m have to be reloaded (because of active_test) # (eg. product.template: product_variant_ids) self.flush() self.invalidate_cache() # `_get_first_possible_variant_id` depends on variants active state self.clear_caches() return res def unlink(self): unlink_products = self.env['product.product'] unlink_templates = self.env['product.template'] for product in self: # If there is an image set on the variant and no image set on the # template, move the image to the template. if product.image_variant_1920 and not product.product_tmpl_id.image_1920: product.product_tmpl_id.image_1920 = product.image_variant_1920 # Check if product still exists, in case it has been unlinked by unlinking its template if not product.exists(): continue # Check if the product is last product of this template... other_products = self.search([('product_tmpl_id', '=', product.product_tmpl_id.id), ('id', '!=', product.id)]) # ... and do not delete product template if it's configured to be created "on demand" if not other_products and not product.product_tmpl_id.has_dynamic_attributes(): unlink_templates |= product.product_tmpl_id unlink_products |= product res = super(ProductProduct, unlink_products).unlink() # delete templates after calling super, as deleting template could lead to deleting # products due to ondelete='cascade' unlink_templates.unlink() # `_get_variant_id_for_combination` depends on existing variants self.clear_caches() return res def _unlink_or_archive(self, check_access=True): """Unlink or archive products. Try in batch as much as possible because it is much faster. Use dichotomy when an exception occurs. """ # Avoid access errors in case the products is shared amongst companies # but the underlying objects are not. If unlink fails because of an # AccessError (e.g. while recomputing fields), the 'write' call will # fail as well for the same reason since the field has been set to # recompute. if check_access: self.check_access_rights('unlink') self.check_access_rule('unlink') self.check_access_rights('write') self.check_access_rule('write') self = self.sudo() try: with self.env.cr.savepoint(), tools.mute_logger('odoo.sql_db'): self.unlink() except Exception: # We catch all kind of exceptions to be sure that the operation # doesn't fail. if len(self) > 1: self[:len(self) // 2]._unlink_or_archive(check_access=False) self[len(self) // 2:]._unlink_or_archive(check_access=False) else: if self.active: # Note: this can still fail if something is preventing # from archiving. # This is the case from existing stock reordering rules. self.write({'active': False}) @api.returns('self', lambda value: value.id) def copy(self, default=None): """Variants are generated depending on the configuration of attributes and values on the template, so copying them does not make sense. For convenience the template is copied instead and its first variant is returned. """ return self.product_tmpl_id.copy(default=default).product_variant_id @api.model def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None): # TDE FIXME: strange if self._context.get('search_default_categ_id'): args.append((('categ_id', 'child_of', self._context['search_default_categ_id']))) return super(ProductProduct, self)._search(args, offset=offset, limit=limit, order=order, count=count, access_rights_uid=access_rights_uid) def name_get(self): # TDE: this could be cleaned a bit I think def _name_get(d): name = d.get('name', '') code = self._context.get('display_default_code', True) and d.get('default_code', False) or False if code: name = '[%s] %s' % (code,name) return (d['id'], name) partner_id = self._context.get('partner_id') if partner_id: partner_ids = [partner_id, self.env['res.partner'].browse(partner_id).commercial_partner_id.id] else: partner_ids = [] company_id = self.env.context.get('company_id') # all user don't have access to seller and partner # check access and use superuser self.check_access_rights("read") self.check_access_rule("read") result = [] # Prefetch the fields used by the `name_get`, so `browse` doesn't fetch other fields # Use `load=False` to not call `name_get` for the `product_tmpl_id` self.sudo().read(['name', 'default_code', 'product_tmpl_id'], load=False) product_template_ids = self.sudo().mapped('product_tmpl_id').ids if partner_ids: supplier_info = self.env['product.supplierinfo'].sudo().search([ ('product_tmpl_id', 'in', product_template_ids), ('name', 'in', partner_ids), ]) # Prefetch the fields used by the `name_get`, so `browse` doesn't fetch other fields # Use `load=False` to not call `name_get` for the `product_tmpl_id` and `product_id` supplier_info.sudo().read(['product_tmpl_id', 'product_id', 'product_name', 'product_code'], load=False) supplier_info_by_template = {} for r in supplier_info: supplier_info_by_template.setdefault(r.product_tmpl_id, []).append(r) for product in self.sudo(): variant = product.product_template_attribute_value_ids._get_combination_name() name = variant and "%s (%s)" % (product.name, variant) or product.name sellers = [] if partner_ids: product_supplier_info = supplier_info_by_template.get(product.product_tmpl_id, []) sellers = [x for x in product_supplier_info if x.product_id and x.product_id == product] if not sellers: sellers = [x for x in product_supplier_info if not x.product_id] # Filter out sellers based on the company. This is done afterwards for a better # code readability. At this point, only a few sellers should remain, so it should # not be a performance issue. if company_id: sellers = [x for x in sellers if x.company_id.id in [company_id, False]] if sellers: for s in sellers: seller_variant = s.product_name and ( variant and "%s (%s)" % (s.product_name, variant) or s.product_name ) or False mydict = { 'id': product.id, 'name': seller_variant or name, 'default_code': s.product_code or product.default_code, } temp = _name_get(mydict) if temp not in result: result.append(temp) else: mydict = { 'id': product.id, 'name': name, 'default_code': product.default_code, } result.append(_name_get(mydict)) return result @api.model def _name_search(self, name, args=None, operator='ilike', limit=100, name_get_uid=None): if not args: args = [] if name: positive_operators = ['=', 'ilike', '=ilike', 'like', '=like'] product_ids = [] if operator in positive_operators: product_ids = self._search([('default_code', '=', name)] + args, limit=limit, access_rights_uid=name_get_uid) if not product_ids: product_ids = self._search([('barcode', '=', name)] + args, limit=limit, access_rights_uid=name_get_uid) if not product_ids and operator not in expression.NEGATIVE_TERM_OPERATORS: # Do not merge the 2 next lines into one single search, SQL search performance would be abysmal # on a database with thousands of matching products, due to the huge merge+unique needed for the # OR operator (and given the fact that the 'name' lookup results come from the ir.translation table # Performing a quick memory merge of ids in Python will give much better performance product_ids = self._search(args + [('default_code', operator, name)], limit=limit) if not limit or len(product_ids) < limit: # we may underrun the limit because of dupes in the results, that's fine limit2 = (limit - len(product_ids)) if limit else False product2_ids = self._search(args + [('name', operator, name), ('id', 'not in', product_ids)], limit=limit2, access_rights_uid=name_get_uid) product_ids.extend(product2_ids) elif not product_ids and operator in expression.NEGATIVE_TERM_OPERATORS: domain = expression.OR([ ['&', ('default_code', operator, name), ('name', operator, name)], ['&', ('default_code', '=', False), ('name', operator, name)], ]) domain = expression.AND([args, domain]) product_ids = self._search(domain, limit=limit, access_rights_uid=name_get_uid) if not product_ids and operator in positive_operators: ptrn = re.compile('(\[(.*?)\])') res = ptrn.search(name) if res: product_ids = self._search([('default_code', '=', res.group(2))] + args, limit=limit, access_rights_uid=name_get_uid) # still no results, partner in context: search on supplier info as last hope to find something if not product_ids and self._context.get('partner_id'): suppliers_ids = self.env['product.supplierinfo']._search([ ('name', '=', self._context.get('partner_id')), '|', ('product_code', operator, name), ('product_name', operator, name)], access_rights_uid=name_get_uid) if suppliers_ids: product_ids = self._search([('product_tmpl_id.seller_ids', 'in', suppliers_ids)], limit=limit, access_rights_uid=name_get_uid) else: product_ids = self._search(args, limit=limit, access_rights_uid=name_get_uid) return models.lazy_name_get(self.browse(product_ids).with_user(name_get_uid)) @api.model def view_header_get(self, view_id, view_type): res = super(ProductProduct, self).view_header_get(view_id, view_type) if self._context.get('categ_id'): return _('Products: ') + self.env['product.category'].browse(self._context['categ_id']).name return res def open_pricelist_rules(self): self.ensure_one() domain = ['|', '&', ('product_tmpl_id', '=', self.product_tmpl_id.id), ('applied_on', '=', '1_product'), '&', ('product_id', '=', self.id), ('applied_on', '=', '0_product_variant')] return { 'name': _('Price Rules'), 'view_mode': 'tree,form', 'views': [(self.env.ref('product.product_pricelist_item_tree_view_from_product').id, 'tree'), (False, 'form')], 'res_model': 'product.pricelist.item', 'type': 'ir.actions.act_window', 'target': 'current', 'domain': domain, 'context': { 'default_product_id': self.id, 'default_applied_on': '0_product_variant', } } def open_product_template(self): """ Utility method used to add an "Open Template" button in product views """ self.ensure_one() return {'type': 'ir.actions.act_window', 'res_model': 'product.template', 'view_mode': 'form', 'res_id': self.product_tmpl_id.id, 'target': 'new'} def _prepare_sellers(self, params): # This search is made to avoid retrieving seller_ids from the cache. return self.env['product.supplierinfo'].search([('product_tmpl_id', '=', self.product_tmpl_id.id), ('name.active', '=', True)]).sorted(lambda s: (s.sequence, -s.min_qty, s.price, s.id)) def _select_seller(self, partner_id=False, quantity=0.0, date=None, uom_id=False, params=False): self.ensure_one() if date is None: date = fields.Date.context_today(self) precision = self.env['decimal.precision'].precision_get('Product Unit of Measure') res = self.env['product.supplierinfo'] sellers = self._prepare_sellers(params) if self.env.context.get('force_company'): sellers = sellers.filtered(lambda s: not s.company_id or s.company_id.id == self.env.context['force_company']) for seller in sellers: # Set quantity in UoM of seller quantity_uom_seller = quantity if quantity_uom_seller and uom_id and uom_id != seller.product_uom: quantity_uom_seller = uom_id._compute_quantity(quantity_uom_seller, seller.product_uom) if seller.date_start and seller.date_start > date: continue if seller.date_end and seller.date_end < date: continue if partner_id and seller.name not in [partner_id, partner_id.parent_id]: continue if float_compare(quantity_uom_seller, seller.min_qty, precision_digits=precision) == -1: continue if seller.product_id and seller.product_id != self: continue if not res or res.name == seller.name: res |= seller return res.sorted('price')[:1] def price_compute(self, price_type, uom=False, currency=False, company=False): # TDE FIXME: delegate to template or not ? fields are reencoded here ... # compatibility about context keys used a bit everywhere in the code if not uom and self._context.get('uom'): uom = self.env['uom.uom'].browse(self._context['uom']) if not currency and self._context.get('currency'): currency = self.env['res.currency'].browse(self._context['currency']) products = self if price_type == 'standard_price': # standard_price field can only be seen by users in base.group_user # Thus, in order to compute the sale price from the cost for users not in this group # We fetch the standard price as the superuser products = self.with_context(force_company=company and company.id or self._context.get('force_company', self.env.company.id)).sudo() prices = dict.fromkeys(self.ids, 0.0) for product in products: prices[product.id] = product[price_type] or 0.0 if price_type == 'list_price': prices[product.id] += product.price_extra # we need to add the price from the attributes that do not generate variants # (see field product.attribute create_variant) if self._context.get('no_variant_attributes_price_extra'): # we have a list of price_extra that comes from the attribute values, we need to sum all that prices[product.id] += sum(self._context.get('no_variant_attributes_price_extra')) if uom: prices[product.id] = product.uom_id._compute_price(prices[product.id], uom) # Convert from current user company currency to asked one # This is right cause a field cannot be in more than one currency if currency: prices[product.id] = product.currency_id._convert( prices[product.id], currency, product.company_id, fields.Date.today()) return prices @api.model def get_empty_list_help(self, help): self = self.with_context( empty_list_help_document_name=_("product"), ) return super(ProductProduct, self).get_empty_list_help(help) def get_product_multiline_description_sale(self): """ Compute a multiline description of this product, in the context of sales (do not use for purchases or other display reasons that don't intend to use "description_sale"). It will often be used as the default description of a sale order line referencing this product. """ name = self.display_name if self.description_sale: name += '\n' + self.description_sale return name def _is_variant_possible(self, parent_combination=None): """Return whether the variant is possible based on its own combination, and optionally a parent combination. See `_is_combination_possible` for more information. This will always exclude variants for templates that have `no_variant` attributes because the variant itself will not be the full combination. :param parent_combination: combination from which `self` is an optional or accessory product. :type parent_combination: recordset `product.template.attribute.value` :return: ẁhether the variant is possible based on its own combination :rtype: bool """ self.ensure_one() return self.product_tmpl_id._is_combination_possible(self.product_template_attribute_value_ids, parent_combination=parent_combination) def toggle_active(self): """ Archiving related product.template if there is only one active product.product """ with_one_active = self.filtered(lambda product: len(product.product_tmpl_id.with_context(active_test=False).product_variant_ids) == 1) for product in with_one_active: product.product_tmpl_id.toggle_active() return super(ProductProduct, self - with_one_active).toggle_active()
class MisReportInstance(models.Model): """The MIS report instance combines everything to compute a MIS report template for a set of periods.""" @api.depends("date") def _compute_pivot_date(self): for record in self: if record.date: record.pivot_date = record.date else: record.pivot_date = fields.Date.context_today(record) @api.model def _default_company_id(self): default_company_id = self.env["res.company"]._company_default_get( "mis.report.instance") return default_company_id _name = "mis.report.instance" _description = "MIS Report Instance" name = fields.Char(required=True, string="Name", translate=True) description = fields.Char(related="report_id.description", readonly=True) date = fields.Date(string="Base date", help="Report base date " "(leave empty to use current date)") pivot_date = fields.Date(compute="_compute_pivot_date", string="Pivot date") report_id = fields.Many2one("mis.report", required=True, string="Report") period_ids = fields.One2many( comodel_name="mis.report.instance.period", inverse_name="report_instance_id", required=True, string="Periods", copy=True, ) target_move = fields.Selection( [("posted", "All Posted Entries"), ("all", "All Entries")], string="Target Moves", required=True, default="posted", ) company_id = fields.Many2one( comodel_name="res.company", string="Company", default=_default_company_id, required=True, ) multi_company = fields.Boolean( string="Multiple companies", help="Check if you wish to specify " "children companies to be searched for data.", default=False, ) company_ids = fields.Many2many( comodel_name="res.company", string="Companies", help="Select companies for which data will be searched.", ) query_company_ids = fields.Many2many( comodel_name="res.company", compute="_compute_query_company_ids", help="Companies for which data will be searched.", ) currency_id = fields.Many2one( comodel_name="res.currency", string="Currency", help="Select target currency for the report. " "Required if companies have different currencies.", required=False, ) landscape_pdf = fields.Boolean(string="Landscape PDF") no_auto_expand_accounts = fields.Boolean( string="Disable account details expansion") display_columns_description = fields.Boolean( help="Display the date range details in the column headers.") comparison_mode = fields.Boolean(compute="_compute_comparison_mode", inverse="_inverse_comparison_mode") date_range_id = fields.Many2one(comodel_name="date.range", string="Date Range") date_from = fields.Date(string="From") date_to = fields.Date(string="To") temporary = fields.Boolean(default=False) analytic_account_id = fields.Many2one( comodel_name="account.analytic.account", string="Analytic Account", oldname="account_analytic_id", ) analytic_tag_ids = fields.Many2many(comodel_name="account.analytic.tag", string="Analytic Tags") hide_analytic_filters = fields.Boolean(default=True) @api.onchange("company_id", "multi_company") def _onchange_company(self): if self.company_id and self.multi_company: self.company_ids = self.env["res.company"].search([ ("id", "child_of", self.company_id.id) ]) else: self.company_ids = False @api.depends("multi_company", "company_id", "company_ids") def _compute_query_company_ids(self): for rec in self: if rec.multi_company: rec.query_company_ids = rec.company_ids or rec.company_id else: rec.query_company_ids = rec.company_id @api.model def get_filter_descriptions_from_context(self): filters = self.env.context.get("mis_report_filters", {}) analytic_account_id = filters.get("analytic_account_id", {}).get("value") filter_descriptions = [] if analytic_account_id: analytic_account = self.env["account.analytic.account"].browse( analytic_account_id) filter_descriptions.append( _("Analytic Account: %s") % analytic_account.display_name) analytic_tag_value = filters.get("analytic_tag_ids", {}).get("value") if analytic_tag_value: analytic_tag_names = self.resolve_2many_commands( "analytic_tag_ids", analytic_tag_value, ["name"]) filter_descriptions.append( _("Analytic Tags: %s") % ", ".join([rec["name"] for rec in analytic_tag_names])) return filter_descriptions def save_report(self): self.ensure_one() self.write({"temporary": False}) action = self.env.ref("mis_builder.mis_report_instance_view_action") res = action.read()[0] view = self.env.ref("mis_builder.mis_report_instance_view_form") res.update({"views": [(view.id, "form")], "res_id": self.id}) return res @api.model def _vacuum_report(self, hours=24): clear_date = fields.Datetime.to_string(datetime.datetime.now() - datetime.timedelta(hours=hours)) reports = self.search([("write_date", "<", clear_date), ("temporary", "=", True)]) _logger.debug("Vacuum %s Temporary MIS Builder Report", len(reports)) return reports.unlink() def copy(self, default=None): self.ensure_one() default = dict(default or {}) default["name"] = _("%s (copy)") % self.name return super(MisReportInstance, self).copy(default) def _format_date(self, date): # format date following user language lang_model = self.env["res.lang"] lang = lang_model._lang_get(self.env.user.lang) date_format = lang.date_format return datetime.datetime.strftime(fields.Date.from_string(date), date_format) @api.depends("date_from") def _compute_comparison_mode(self): for instance in self: instance.comparison_mode = bool( instance.period_ids) and not bool(instance.date_from) def _inverse_comparison_mode(self): for record in self: if not record.comparison_mode: if not record.date_from: record.date_from = fields.Date.context_today(self) if not record.date_to: record.date_to = fields.Date.context_today(self) record.period_ids.unlink() record.write({"period_ids": [(0, 0, {"name": "Default"})]}) else: record.date_from = None record.date_to = None @api.onchange("date_range_id") def _onchange_date_range(self): if self.date_range_id: self.date_from = self.date_range_id.date_start self.date_to = self.date_range_id.date_end @api.onchange("date_from", "date_to") def _onchange_dates(self): if self.date_range_id: if (self.date_from != self.date_range_id.date_start or self.date_to != self.date_range_id.date_end): self.date_range_id = False def _add_analytic_filters_to_context(self, context): self.ensure_one() if self.analytic_account_id: context["mis_report_filters"]["analytic_account_id"] = { "value": self.analytic_account_id.id, "operator": "=", } if self.analytic_tag_ids: context["mis_report_filters"]["analytic_tag_ids"] = { "value": self.analytic_tag_ids.ids, "operator": "all", } def _context_with_filters(self): self.ensure_one() if "mis_report_filters" in self.env.context: # analytic filters are already in context, do nothing return self.env.context context = dict(self.env.context, mis_report_filters={}) self._add_analytic_filters_to_context(context) return context def preview(self): self.ensure_one() view_id = self.env.ref("mis_builder." "mis_report_instance_result_view_form") return { "type": "ir.actions.act_window", "res_model": "mis.report.instance", "res_id": self.id, "view_mode": "form", "view_type": "form", "view_id": view_id.id, "target": "current", "context": self._context_with_filters(), } def print_pdf(self): self.ensure_one() context = dict(self._context_with_filters(), landscape=self.landscape_pdf) return (self.env.ref("mis_builder.qweb_pdf_export").with_context( context).report_action( self, data=dict(dummy=True)) # required to propagate context ) def export_xls(self): self.ensure_one() context = dict(self._context_with_filters()) return (self.env.ref("mis_builder.xls_export").with_context( context).report_action( self, data=dict(dummy=True)) # required to propagate context ) def display_settings(self): assert len(self.ids) <= 1 view_id = self.env.ref("mis_builder.mis_report_instance_view_form") return { "type": "ir.actions.act_window", "res_model": "mis.report.instance", "res_id": self.id if self.id else False, "view_mode": "form", "view_type": "form", "views": [(view_id.id, "form")], "view_id": view_id.id, "target": "current", } def _add_column_move_lines(self, aep, kpi_matrix, period, label, description): if not period.date_from or not period.date_to: raise UserError( _("Column %s with move lines source must have from/to dates.") % (period.name, )) expression_evaluator = ExpressionEvaluator( aep, period.date_from, period.date_to, None, # target_move now part of additional_move_line_filter period._get_additional_move_line_filter(), period._get_aml_model_name(), ) self.report_id._declare_and_compute_period( expression_evaluator, kpi_matrix, period.id, label, description, period.subkpi_ids, period._get_additional_query_filter, no_auto_expand_accounts=self.no_auto_expand_accounts, ) def _add_column_sumcol(self, aep, kpi_matrix, period, label, description): kpi_matrix.declare_sum( period.id, [(c.sign, c.period_to_sum_id.id) for c in period.source_sumcol_ids], label, description, period.source_sumcol_accdet, ) def _add_column_cmpcol(self, aep, kpi_matrix, period, label, description): kpi_matrix.declare_comparison( period.id, period.source_cmpcol_to_id.id, period.source_cmpcol_from_id.id, label, description, ) def _add_column(self, aep, kpi_matrix, period, label, description): if period.source == SRC_ACTUALS: return self._add_column_move_lines(aep, kpi_matrix, period, label, description) elif period.source == SRC_ACTUALS_ALT: return self._add_column_move_lines(aep, kpi_matrix, period, label, description) elif period.source == SRC_SUMCOL: return self._add_column_sumcol(aep, kpi_matrix, period, label, description) elif period.source == SRC_CMPCOL: return self._add_column_cmpcol(aep, kpi_matrix, period, label, description) def _compute_matrix(self): """ Compute a report and return a KpiMatrix. The key attribute of the matrix columns (KpiMatrixCol) is guaranteed to be the id of the mis.report.instance.period. """ self.ensure_one() aep = self.report_id._prepare_aep(self.query_company_ids, self.currency_id) kpi_matrix = self.report_id.prepare_kpi_matrix(self.multi_company) for period in self.period_ids: description = None if period.mode == MODE_NONE: pass elif not self.display_columns_description: pass elif period.date_from == period.date_to and period.date_from: description = self._format_date(period.date_from) elif period.date_from and period.date_to: date_from = self._format_date(period.date_from) date_to = self._format_date(period.date_to) description = _("from %s to %s") % (date_from, date_to) self._add_column(aep, kpi_matrix, period, period.name, description) kpi_matrix.compute_comparisons() kpi_matrix.compute_sums() return kpi_matrix def compute(self): self.ensure_one() kpi_matrix = self._compute_matrix() return kpi_matrix.as_dict() def drilldown(self, arg): self.ensure_one() period_id = arg.get("period_id") expr = arg.get("expr") account_id = arg.get("account_id") if period_id and expr and AEP.has_account_var(expr): period = self.env["mis.report.instance.period"].browse(period_id) aep = AEP(self.query_company_ids, self.currency_id, self.report_id.account_model) aep.parse_expr(expr) aep.done_parsing() domain = aep.get_aml_domain_for_expr( expr, period.date_from, period.date_to, None, # target_move now part of additional_move_line_filter account_id, ) domain.extend(period._get_additional_move_line_filter()) return { "name": u"{} - {}".format(expr, period.name), "domain": domain, "type": "ir.actions.act_window", "res_model": period._get_aml_model_name(), "views": [[False, "list"], [False, "form"]], "view_type": "list", "view_mode": "list", "target": "current", "context": { "active_test": False }, } else: return False
class Order(models.Model): _name = 'nursery.order' _description = 'Plant Order' _inherit = ['mail.thread', 'mail.activity.mixin', 'rating.mixin', 'utm.mixin', 'portal.mixin'] name = fields.Char( 'Reference', default=lambda self: _('New'), required=True, states={'draft': [('readonly', False)]}) user_id = fields.Many2one( 'res.users', string='Responsible', index=True, required=True, default=lambda self: self.env.user) date_open = fields.Date( 'Confirmation date', readonly=True) customer_id = fields.Many2one( "nursery.customer", string='Customer', index=True, required=True) line_ids = fields.One2many( 'nursery.order.line', 'order_id', string='Order Lines') amount_total = fields.Float( 'Amount', compute='_compute_amount_total', store=True) company_id = fields.Many2one( 'res.company', string='Company', related='user_id.company_id', readonly=True, store=True) currency_id = fields.Many2one( 'res.currency', string='Currency', related='company_id.currency_id', readonly=True, required=True) state = fields.Selection([ ('draft', 'Draft'), ('open', 'Open'), ('done', 'Done'), ('cancel', 'Canceled'), ], default='draft', index=True, group_expand="_expand_states") last_modification = fields.Datetime(readonly=True) @api.depends('line_ids.price') def _compute_amount_total(self): for order in self: order.amount_total = sum(order.mapped('line_ids.price')) def _compute_access_url(self): super(Order, self)._compute_access_url() for order in self: order.access_url = '/my/order/%s' % order.id def action_confirm(self): if self.state != 'draft': return self.activity_feedback(['mail.mail_activity_data_todo']) return self.write({ 'state': 'open', 'date_open': fields.Datetime.now(), }) @api.model def create(self, vals): if vals.get('name', _('New')) == _('New'): if 'company_id' in vals: vals['name'] = self.env['ir.sequence'].with_context(force_company=vals['company_id']).next_by_code('plant.order') or _('New') else: vals['name'] = self.env['ir.sequence'].next_by_code('plant.order') or _('New') res = super(Order, self).create(vals) res.activity_schedule( 'mail.mail_activity_data_todo', user_id=self.env.ref('base.user_demo').id, date_deadline=fields.Date.today() + relativedelta(days=1), summary=_('Pack the order')) return res def write(self, values): # helper to "YYYY-MM-DD" values['last_modification'] = fields.Datetime.now() return super(Order, self).write(values) def unlink(self): # self is a recordset for order in self: if order.state == 'confirm': raise UserError("You can not delete confirmed orders") return super(Order, self).unlink() def _expand_states(self, states, domain, order): return [key for key, val in type(self).state.selection] def action_get_ratings(self): action = self.env['ir.actions.act_window'].for_xml_id('rating', 'action_view_rating') return dict( action, domain=[('res_id', 'in', self.ids), ('res_model', '=', 'nursery.order')], ) def action_send_rating(self): rating_template = self.env.ref('plant_nursery.mail_template_plant_order_rating') for order in self: order.rating_send_request(rating_template, force_send=True) def rating_get_partner_id(self): if self.customer_id.partner_id: return self.customer_id.partner_id return self.env['res.partner'] def message_new(self, msg_dict, custom_values=None): if custom_values is None: custom_values = {} if custom_values.get('category_id', False): domain = [('category_id', '=', custom_values.pop('category_id'))] custom_values.pop else: domain = [] # find or create customer email = email_split(msg_dict.get('email_from', False))[0] name = email_split_and_format(msg_dict.get('email_from', False))[0] customer = self.env['nursery.customer'].find_or_create(email, name) # happy Xmas plants = self.env['nursery.plant'].search(domain) plant = self.env['nursery.plant'].browse([random.choice(plants.ids)]) custom_values.update({ 'customer_id': customer.id, 'line_ids': [(4, plant.id)], }) return super(Order, self).message_new(msg_dict, custom_values=custom_values)
class SaleOrder(models.Model): _inherit = "sale.order" third_party_order = fields.Boolean(default=False, readonly=True, states={"draft": [("readonly", False)]}) third_party_partner_id = fields.Many2one( comodel_name="res.partner", readonly=True, states={"draft": [("readonly", False)]}, domain=[ ("supplier", "=", True), ("third_party_sequence_id", "!=", False), ], ) third_party_number = fields.Char(copy=False, readonly=True) third_party_move_id = fields.Many2one(comodel_name="account.move", string="Third party move", readonly=True) source_third_party_order_id = fields.Many2one(comodel_name="sale.order", string="Third party order", readonly=True) third_party_order_ids = fields.One2many( comodel_name="sale.order", inverse_name="source_third_party_order_id", string="Third party orders", readonly=True, ) third_party_order_count = fields.Integer( string="Third party order #", compute="_compute_third_party_order_count", readonly=True, ) third_party_customer_in_state = fields.Selection( [("pending", "Pending amount"), ("paid", "Fully paid")], compute="_compute_third_party_residual", store=True, index=True, ) string = ("Incoming payment status", ) third_party_customer_in_residual = fields.Monetary( currency_field="currency_id", compute="_compute_third_party_residual", string="Incoming payment residual amount", ) third_party_customer_in_residual_company = fields.Monetary( currency_field="currency_id", compute="_compute_third_party_residual", string="Incoming payment residual amount in company currency", ) third_party_customer_out_state = fields.Selection( [("pending", "Pending amount"), ("paid", "Fully paid")], string="Outgoing payment status", compute="_compute_third_party_residual", store=True, index=True, ) third_party_customer_out_residual = fields.Monetary( currency_field="currency_id", string="Outgoing payment residual amount", compute="_compute_third_party_residual", ) third_party_customer_out_residual_company = fields.Monetary( currency_field="currency_id", string="Outgoing payment residual amount in company currency", compute="_compute_third_party_residual", ) @api.multi @api.depends( "third_party_order", "third_party_move_id", "third_party_move_id.line_ids.amount_residual", ) def _compute_third_party_residual(self): """Computes residual amounts from Journal items.""" for rec in self: rec.third_party_customer_in_state = "pending" rec.third_party_customer_out_state = "pending" third_party_customer_account = rec.partner_id.with_context( force_company=rec.company_id.id ).property_third_party_customer_account_id third_party_supplier_account = rec.third_party_partner_id.with_context( force_company=rec.company_id.id ).property_third_party_supplier_account_id in_residual = 0.0 in_residual_company = 0.0 out_residual = 0.0 out_residual_company = 0.0 for line in rec.sudo().third_party_move_id.line_ids: if (line.account_id == third_party_customer_account and line.partner_id == rec.partner_id): in_residual_company += line.amount_residual if line.currency_id == rec.currency_id: in_residual += (line.amount_residual_currency if line.currency_id else line.amount_residual) else: from_currency = ( line.currency_id and line.currency_id.with_context(date=line.date) ) or line.company_id.currency_id.with_context( date=line.date) in_residual += from_currency._convert( line.amount_residual, rec.currency_id, line.company_id, line.date, ) elif (line.account_id == third_party_supplier_account and line.partner_id == rec.third_party_partner_id): out_residual_company += line.amount_residual if line.currency_id == rec.currency_id: out_residual += (line.amount_residual_currency if line.currency_id else line.amount_residual) else: from_currency = ( line.currency_id and line.currency_id.with_context(date=line.date) ) or line.company_id.currency_id.with_context( date=line.date) out_residual += from_currency._convert( line.amount_residual, rec.currency_id, line.company_id, line.date, ) rec.third_party_customer_in_residual_company = abs( in_residual_company) rec.third_party_customer_in_residual = abs(in_residual) rec.third_party_customer_out_residual_company = abs( out_residual_company) rec.third_party_customer_out_residual = abs(out_residual) if float_is_zero( rec.third_party_customer_in_residual, precision_rounding=rec.currency_id.rounding, ): rec.third_party_customer_in_state = "paid" if float_is_zero( rec.third_party_customer_out_residual, precision_rounding=rec.currency_id.rounding, ): rec.third_party_customer_out_state = "paid" @api.multi def _compute_third_party_order_count(self): for order in self: order.third_party_order_count = len(order.third_party_order_ids) def create_third_party_move(self): self.third_party_move_id = self.env["account.move"].create( self._third_party_move_vals()) self.third_party_move_id.post() def _third_party_move_vals(self): journal = self.company_id.third_party_journal_id if not journal: journal = self.env["account.journal"].search( [ ("company_id", "=", self.company_id.id), ("type", "=", "general"), ], limit=1, ) third_party_customer_account = self.partner_id.with_context( force_company=self.company_id.id ).property_third_party_customer_account_id third_party_supplier_account = self.third_party_partner_id.with_context( force_company=self.company_id.id ).property_third_party_supplier_account_id if not third_party_customer_account: raise UserError( _("Please define a third party customer account " "for %s." % self.partner_id.name)) if not third_party_supplier_account: raise UserError( _("Please define a third party supplier account " "for %s." % self.third_party_partner_id.name)) return { "journal_id": journal.id, "line_ids": [ ( 0, 0, { "name": self.partner_id.name, "partner_id": self.partner_id.id, "account_id": third_party_customer_account.id, "debit": self.amount_total, "credit": 0, }, ), ( 0, 0, { "name": self.third_party_partner_id.name, "partner_id": self.third_party_partner_id.id, "account_id": third_party_supplier_account.id, "debit": 0, "credit": self.amount_total, }, ), ], } def _prepare_third_party_order(self): lines = self.order_line.filtered(lambda l: l.third_party_product_id) so_lines = [(0, 0, l._prepare_third_party_order_line()) for l in lines] return { "partner_id": self.third_party_partner_id.id, "fiscal_position_id": self.third_party_partner_id.with_context( force_company=self.company_id.id).property_account_position_id. id or False, "order_line": so_lines, "company_id": self.company_id.id, "source_third_party_order_id": self.id, } @api.model def _create_third_party_order(self): vals = self._prepare_third_party_order() order = self.env["sale.order"].create(vals) order._compute_tax_id() return order @api.multi def _action_confirm(self): res = super(SaleOrder, self)._action_confirm() for order in self.filtered(lambda o: o.third_party_order): if not order.third_party_number and not self.env.context.get( "no_third_party_number", False): sequence = order.third_party_partner_id.third_party_sequence_id if not sequence: raise UserError( _("Please define an invoice " "sequence in the third party partner.")) order.third_party_number = sequence.next_by_id() order.create_third_party_move() order._create_third_party_order() return res @api.multi def action_confirm(self): res = super(SaleOrder, self).action_confirm() for order in self.filtered(lambda o: o.third_party_order): order.action_done() return res @api.multi def action_cancel(self): res = super(SaleOrder, self).action_cancel() for order in self.filtered(lambda o: o.third_party_move_id): order.third_party_order_ids.action_cancel() order.third_party_move_id.button_cancel() order.third_party_move_id.unlink() return res def action_view_third_party_order(self): action = self.env.ref("sale.action_orders") result = action.read()[0] order_ids = self.third_party_order_ids.ids if len(order_ids) != 1: result["domain"] = [("id", "in", order_ids)] elif len(order_ids) == 1: res = self.env.ref("sale.view_order_form", False) result["views"] = [(res and res.id or False, "form")] result["res_id"] = order_ids[0] return result @api.constrains("third_party_order", "third_party_partner_id") def _check_third_party_constrains(self): for rec in self: if rec.third_party_order and not rec.third_party_partner_id: raise ValidationError( _("Please define a third party partner.")) @api.multi def third_party_invoice_print(self): return self.env.ref( "sale_third_party.action_report_saleorder_third_party" ).report_action(self)
class PurchaseOrder(models.Model): _name = "purchase.order" _inherit = ['mail.thread', 'mail.activity.mixin', 'portal.mixin'] _description = "Purchase Order" _order = 'date_order desc, id desc' def _default_currency_id(self): company_id = self.env.context.get( 'force_company') or self.env.context.get( 'company_id') or self.env.user.company_id.id return self.env['res.company'].browse(company_id).currency_id @api.depends('order_line.price_total') def _amount_all(self): for order in self: amount_untaxed = amount_tax = 0.0 for line in order.order_line: amount_untaxed += line.price_subtotal amount_tax += line.price_tax order.update({ 'amount_untaxed': order.currency_id.round(amount_untaxed), 'amount_tax': order.currency_id.round(amount_tax), 'amount_total': amount_untaxed + amount_tax, }) @api.depends('order_line.date_planned', 'date_order') def _compute_date_planned(self): for order in self: min_date = False for line in order.order_line: if not min_date or line.date_planned < min_date: min_date = line.date_planned if min_date: order.date_planned = min_date else: order.date_planned = order.date_order @api.depends('state', 'order_line.qty_invoiced', 'order_line.qty_received', 'order_line.product_qty') def _get_invoiced(self): precision = self.env['decimal.precision'].precision_get( 'Product Unit of Measure') for order in self: if order.state not in ('purchase', 'done'): order.invoice_status = 'no' continue if any( float_compare( line.qty_invoiced, line.product_qty if line.product_id.purchase_method == 'purchase' else line.qty_received, precision_digits=precision) == -1 for line in order.order_line): order.invoice_status = 'to invoice' elif all( float_compare( line.qty_invoiced, line.product_qty if line.product_id.purchase_method == 'purchase' else line.qty_received, precision_digits=precision) >= 0 for line in order.order_line) and order.invoice_ids: order.invoice_status = 'invoiced' else: order.invoice_status = 'no' @api.depends('order_line.invoice_lines.invoice_id') def _compute_invoice(self): for order in self: invoices = self.env['account.invoice'] for line in order.order_line: invoices |= line.invoice_lines.mapped('invoice_id') order.invoice_ids = invoices order.invoice_count = len(invoices) READONLY_STATES = { 'purchase': [('readonly', True)], 'done': [('readonly', True)], 'cancel': [('readonly', True)], } name = fields.Char('Order Reference', required=True, index=True, copy=False, default='New') origin = fields.Char( 'Source Document', copy=False, help="Reference of the document that generated this purchase order " "request (e.g. a sales order)") partner_ref = fields.Char( 'Vendor Reference', copy=False, help="Reference of the sales order or bid sent by the vendor. " "It's used to do the matching when you receive the " "products as this reference is usually written on the " "delivery order sent by your vendor.") date_order = fields.Datetime('Order Date', required=True, states=READONLY_STATES, index=True, copy=False, default=fields.Datetime.now,\ help="Depicts the date where the Quotation should be validated and converted into a purchase order.") date_approve = fields.Date('Approval Date', readonly=1, index=True, copy=False) partner_id = fields.Many2one( 'res.partner', string='Vendor', required=True, states=READONLY_STATES, change_default=True, track_visibility='always', help= "You can find a vendor by its Name, TIN, Email or Internal Reference.") dest_address_id = fields.Many2one( 'res.partner', string='Drop Ship Address', states=READONLY_STATES, help= "Put an address if you want to deliver directly from the vendor to the customer. " "Otherwise, keep empty to deliver to your own company.") currency_id = fields.Many2one('res.currency', 'Currency', required=True, states=READONLY_STATES, default=_default_currency_id) state = fields.Selection([('draft', 'RFQ'), ('sent', 'RFQ Sent'), ('to approve', 'To Approve'), ('purchase', 'Purchase Order'), ('done', 'Locked'), ('cancel', 'Cancelled')], string='Status', readonly=True, index=True, copy=False, default='draft', track_visibility='onchange') order_line = fields.One2many('purchase.order.line', 'order_id', string='Order Lines', states={ 'cancel': [('readonly', True)], 'done': [('readonly', True)] }, copy=True) notes = fields.Text('Terms and Conditions') invoice_count = fields.Integer(compute="_compute_invoice", string='Bill Count', copy=False, default=0, store=True) invoice_ids = fields.Many2many('account.invoice', compute="_compute_invoice", string='Bills', copy=False, store=True) invoice_status = fields.Selection([ ('no', 'Nothing to Bill'), ('to invoice', 'Waiting Bills'), ('invoiced', 'No Bill to Receive'), ], string='Billing Status', compute='_get_invoiced', store=True, readonly=True, copy=False, default='no') # There is no inverse function on purpose since the date may be different on each line date_planned = fields.Datetime(string='Scheduled Date', compute='_compute_date_planned', store=True, index=True) amount_untaxed = fields.Monetary(string='Untaxed Amount', store=True, readonly=True, compute='_amount_all', track_visibility='always') amount_tax = fields.Monetary(string='Taxes', store=True, readonly=True, compute='_amount_all') amount_total = fields.Monetary(string='Total', store=True, readonly=True, compute='_amount_all') fiscal_position_id = fields.Many2one('account.fiscal.position', string='Fiscal Position', oldname='fiscal_position') payment_term_id = fields.Many2one('account.payment.term', 'Payment Terms') incoterm_id = fields.Many2one( 'account.incoterms', 'Incoterm', states={'done': [('readonly', True)]}, help= "International Commercial Terms are a series of predefined commercial terms used in international transactions." ) product_id = fields.Many2one('product.product', related='order_line.product_id', string='Product', readonly=False) user_id = fields.Many2one('res.users', string='Purchase Representative', index=True, track_visibility='onchange', default=lambda self: self.env.user) company_id = fields.Many2one( 'res.company', 'Company', required=True, index=True, states=READONLY_STATES, default=lambda self: self.env.user.company_id.id) def _compute_access_url(self): super(PurchaseOrder, self)._compute_access_url() for order in self: order.access_url = '/my/purchase/%s' % (order.id) @api.model def _name_search(self, name, args=None, operator='ilike', limit=100, name_get_uid=None): args = args or [] domain = [] if name: domain = [ '|', ('name', operator, name), ('partner_ref', operator, name) ] purchase_order_ids = self._search(expression.AND([domain, args]), limit=limit, access_rights_uid=name_get_uid) return self.browse(purchase_order_ids).name_get() @api.multi @api.depends('name', 'partner_ref') def name_get(self): result = [] for po in self: name = po.name if po.partner_ref: name += ' (' + po.partner_ref + ')' if self.env.context.get('show_total_amount') and po.amount_total: name += ': ' + formatLang( self.env, po.amount_total, currency_obj=po.currency_id) result.append((po.id, name)) return result @api.model def create(self, vals): company_id = vals.get('company_id', self.default_get(['company_id'])['company_id']) if vals.get('name', 'New') == 'New': vals['name'] = self.env['ir.sequence'].with_context( force_company=company_id).next_by_code('purchase.order') or '/' return super(PurchaseOrder, self.with_context(company_id=company_id)).create(vals) @api.multi def unlink(self): for order in self: if not order.state == 'cancel': raise UserError( _('In order to delete a purchase order, you must cancel it first.' )) return super(PurchaseOrder, self).unlink() @api.multi def copy(self, default=None): ctx = dict(self.env.context) ctx.pop('default_product_id', None) self = self.with_context(ctx) new_po = super(PurchaseOrder, self).copy(default=default) for line in new_po.order_line: seller = line.product_id._select_seller( partner_id=line.partner_id, quantity=line.product_qty, date=line.order_id.date_order and line.order_id.date_order.date(), uom_id=line.product_uom) line.date_planned = line._get_date_planned(seller) return new_po @api.multi def _track_subtype(self, init_values): self.ensure_one() if 'state' in init_values and self.state == 'purchase': return 'purchase.mt_rfq_approved' elif 'state' in init_values and self.state == 'to approve': return 'purchase.mt_rfq_confirmed' elif 'state' in init_values and self.state == 'done': return 'purchase.mt_rfq_done' return super(PurchaseOrder, self)._track_subtype(init_values) @api.onchange('partner_id', 'company_id') def onchange_partner_id(self): if not self.partner_id: self.fiscal_position_id = False self.payment_term_id = False self.currency_id = self.env.user.company_id.currency_id.id else: self.fiscal_position_id = self.env[ 'account.fiscal.position'].with_context( company_id=self.company_id.id).get_fiscal_position( self.partner_id.id) self.payment_term_id = self.partner_id.property_supplier_payment_term_id.id self.currency_id = self.partner_id.property_purchase_currency_id.id or self.env.user.company_id.currency_id.id return {} @api.onchange('fiscal_position_id') def _compute_tax_id(self): """ Trigger the recompute of the taxes if the fiscal position is changed on the PO. """ for order in self: order.order_line._compute_tax_id() @api.onchange('partner_id') def onchange_partner_id_warning(self): if not self.partner_id: return warning = {} title = False message = False partner = self.partner_id # If partner has no warning, check its company if partner.purchase_warn == 'no-message' and partner.parent_id: partner = partner.parent_id if partner.purchase_warn and partner.purchase_warn != 'no-message': # Block if partner only has warning but parent company is blocked if partner.purchase_warn != 'block' and partner.parent_id and partner.parent_id.purchase_warn == 'block': partner = partner.parent_id title = _("Warning for %s") % partner.name message = partner.purchase_warn_msg warning = {'title': title, 'message': message} if partner.purchase_warn == 'block': self.update({'partner_id': False}) return {'warning': warning} return {} @api.multi def action_rfq_send(self): ''' This function opens a window to compose an email, with the edi purchase template message loaded by default ''' self.ensure_one() ir_model_data = self.env['ir.model.data'] try: if self.env.context.get('send_rfq', False): template_id = ir_model_data.get_object_reference( 'purchase', 'email_template_edi_purchase')[1] else: template_id = ir_model_data.get_object_reference( 'purchase', 'email_template_edi_purchase_done')[1] except ValueError: template_id = False try: compose_form_id = ir_model_data.get_object_reference( 'mail', 'email_compose_message_wizard_form')[1] except ValueError: compose_form_id = False ctx = dict(self.env.context or {}) ctx.update({ 'default_model': 'purchase.order', 'default_res_id': self.ids[0], 'default_use_template': bool(template_id), 'default_template_id': template_id, 'default_composition_mode': 'comment', 'custom_layout': "mail.mail_notification_paynow", 'force_email': True, 'mark_rfq_as_sent': True, }) # In the case of a RFQ or a PO, we want the "View..." button in line with the state of the # object. Therefore, we pass the model description in the context, in the language in which # the template is rendered. lang = self.env.context.get('lang') if {'default_template_id', 'default_model', 'default_res_id' } <= ctx.keys(): template = self.env['mail.template'].browse( ctx['default_template_id']) if template and template.lang: lang = template._render_template(template.lang, ctx['default_model'], ctx['default_res_id']) self = self.with_context(lang=lang) if self.state in ['draft', 'sent']: ctx['model_description'] = _('Request for Quotation') else: ctx['model_description'] = _('Purchase Order') return { 'name': _('Compose Email'), 'type': 'ir.actions.act_window', 'view_type': 'form', 'view_mode': 'form', 'res_model': 'mail.compose.message', 'views': [(compose_form_id, 'form')], 'view_id': compose_form_id, 'target': 'new', 'context': ctx, } @api.multi @api.returns('mail.message', lambda value: value.id) def message_post(self, **kwargs): if self.env.context.get('mark_rfq_as_sent'): self.filtered(lambda o: o.state == 'draft').write( {'state': 'sent'}) return super(PurchaseOrder, self.with_context( mail_post_autofollow=True)).message_post(**kwargs) @api.multi def print_quotation(self): self.write({'state': "sent"}) return self.env.ref( 'purchase.report_purchase_quotation').report_action(self) @api.multi def button_approve(self, force=False): self.write({ 'state': 'purchase', 'date_approve': fields.Date.context_today(self) }) self.filtered(lambda p: p.company_id.po_lock == 'lock').write( {'state': 'done'}) return {} @api.multi def button_draft(self): self.write({'state': 'draft'}) return {} @api.multi def button_confirm(self): for order in self: if order.state not in ['draft', 'sent']: continue order._add_supplier_to_product() # Deal with double validation process if order.company_id.po_double_validation == 'one_step'\ or (order.company_id.po_double_validation == 'two_step'\ and order.amount_total < self.env.user.company_id.currency_id._convert( order.company_id.po_double_validation_amount, order.currency_id, order.company_id, order.date_order or fields.Date.today()))\ or order.user_has_groups('purchase.group_purchase_manager'): order.button_approve() else: order.write({'state': 'to approve'}) return True @api.multi def button_cancel(self): for order in self: for inv in order.invoice_ids: if inv and inv.state not in ('cancel', 'draft'): raise UserError( _("Unable to cancel this purchase order. You must first cancel the related vendor bills." )) self.write({'state': 'cancel'}) @api.multi def button_unlock(self): self.write({'state': 'purchase'}) @api.multi def button_done(self): self.write({'state': 'done'}) @api.multi def _add_supplier_to_product(self): # Add the partner in the supplier list of the product if the supplier is not registered for # this product. We limit to 10 the number of suppliers for a product to avoid the mess that # could be caused for some generic products ("Miscellaneous"). for line in self.order_line: # Do not add a contact as a supplier partner = self.partner_id if not self.partner_id.parent_id else self.partner_id.parent_id if partner not in line.product_id.seller_ids.mapped( 'name') and len(line.product_id.seller_ids) <= 10: # Convert the price in the right currency. currency = partner.property_purchase_currency_id or self.env.user.company_id.currency_id price = self.currency_id._convert(line.price_unit, currency, line.company_id, line.date_order or fields.Date.today(), round=False) # Compute the price for the template's UoM, because the supplier's UoM is related to that UoM. if line.product_id.product_tmpl_id.uom_po_id != line.product_uom: default_uom = line.product_id.product_tmpl_id.uom_po_id price = line.product_uom._compute_price(price, default_uom) supplierinfo = { 'name': partner.id, 'sequence': max(line.product_id.seller_ids.mapped('sequence')) + 1 if line.product_id.seller_ids else 1, 'min_qty': 0.0, 'price': price, 'currency_id': currency.id, 'delay': 0, } # In case the order partner is a contact address, a new supplierinfo is created on # the parent company. In this case, we keep the product name and code. seller = line.product_id._select_seller( partner_id=line.partner_id, quantity=line.product_qty, date=line.order_id.date_order and line.order_id.date_order.date(), uom_id=line.product_uom) if seller: supplierinfo['product_name'] = seller.product_name supplierinfo['product_code'] = seller.product_code vals = { 'seller_ids': [(0, 0, supplierinfo)], } try: line.product_id.write(vals) except AccessError: # no write access rights -> just ignore break @api.multi def action_view_invoice(self): ''' This function returns an action that display existing vendor bills of given purchase order ids. When only one found, show the vendor bill immediately. ''' action = self.env.ref('account.action_vendor_bill_template') result = action.read()[0] create_bill = self.env.context.get('create_bill', False) # override the context to get rid of the default filtering result['context'] = { 'type': 'in_invoice', 'default_purchase_id': self.id, 'default_currency_id': self.currency_id.id, 'default_company_id': self.company_id.id, 'company_id': self.company_id.id } # choose the view_mode accordingly if len(self.invoice_ids) > 1 and not create_bill: result['domain'] = "[('id', 'in', " + str( self.invoice_ids.ids) + ")]" else: res = self.env.ref('account.invoice_supplier_form', False) form_view = [(res and res.id or False, 'form')] if 'views' in result: result['views'] = form_view + [ (state, view) for state, view in action['views'] if view != 'form' ] else: result['views'] = form_view # Do not set an invoice_id if we want to create a new bill. if not create_bill: result['res_id'] = self.invoice_ids.id or False result['context']['default_origin'] = self.name result['context']['default_reference'] = self.partner_ref return result @api.multi def action_set_date_planned(self): for order in self: order.order_line.update({'date_planned': order.date_planned})
class KAStockInventoryPriceAdjustment(models.Model): _name = "ka_stock.inventory.price.adjustment" _description = "Stock standard price adjustment and auto create journal" _inherit = ['mail.thread'] _order = 'date desc, id desc' @api.model def _compute_default_location(self): company_user = self.env.user.company_id warehouse = self.env['stock.warehouse'].search( [('company_id', '=', company_user.id)], limit=1) if warehouse: return warehouse.lot_stock_id.id else: raise UserError( _('You must define a warehouse for the company: %s.') % (company_user.name, )) name = fields.Char(string="Reference", copy=False) date = fields.Date(string="Tanggal", default=fields.Datetime.now, track_visibility="onchange") state = fields.Selection([("draft", "Draft"), ("confirm", "In Progress"), ("done", "Validated"), ("cancel", "Cancelled")], string="Status", default="draft", track_visibility="onchange", copy=False) line_ids = fields.One2many("inventory.price.adjustment.line", "price_adjustment_id", string="Products", states={'confirm': [('readonly', True)]}) location_id = fields.Many2one("stock.location", string="Inventoried Location", default=_compute_default_location, track_visibility="onchange") company_id = fields.Many2one("res.company", string="Company", default=lambda self: self.env.user.company_id, track_visibility="onchange") accounting_date = fields.Date(string="Force Accounting", default=fields.Datetime.now, track_visibility="onchange") journal_ids = fields.One2many("account.move", "price_adjustment_id", string="Journals") def action_confirm(self): self.state = "confirm" def action_to_draft(self): self.state = "draft" def action_process(self): #add journal items line_items = [] jumlah_deb = 0 jumlah_kred = 0 for x in self.line_ids: if x.product_id.categ_id == False: raise UserError('Product category pada ' + x.product_id.display_name + 'tidak ditemukan!') else: if x.product_id.categ_id.property_stock_account_input_categ_id == False or \ x.product_id.categ_id.property_stock_account_output_categ_id == False: raise UserError( 'Jurnal masuk/keluar pada category product ' + x.product_id.categ_id + 'tidak ditemukan!') if x.difference < 0: jumlah_kred = +x.difference vals = (0, 0, { 'account_id': x.product_id.categ_id. property_stock_account_output_categ_id.id, 'name': x.product_id.display_name, 'debit': 0, 'credit': abs(x.difference) }) elif x.difference > 0: jumlah_deb = +x.difference vals = (0, 0, { 'account_id': x.product_id.categ_id. property_stock_account_input_categ_id.id, 'name': x.product_id.display_name, 'debit': abs(x.difference), 'credit': 0, }) line_items.append(vals) # set new standard price x.product_id.standard_price = x.standard_price_new #add opposite journal items #-------------Search for Configuration Journal------------- config = self.env[ 'ka_stock.inventory.price.adjustment.configuration'].search( ['company_id', '=', self.company_id.id]) last_vals1 = (0, 0, { 'account_id': 1463, 'name': 'Jurnal balik price adjustment : ' + self.name, 'debit': abs(jumlah_kred), 'credit': 0, }) line_items.append(last_vals1) last_vals2 = (0, 0, { 'account_id': 1463, 'name': 'Jurnal balik price adjustment : ' + self.name, 'debit': 0, 'credit': abs(jumlah_deb), }) line_items.append(last_vals2) line_items.reverse() #create journal data = { 'price_adjustment_id': self.id, 'journal_id': self.env['account.journal'].search([ ('name', '=', 'Stock Journal'), ('company_id', '=', self.env.user.company_id.id) ]).id, 'date': datetime.now(), 'ref': 'Price adjustment : ' + self.name, 'company_id': self.env.user.company_id.id, 'line_ids': line_items, } if self.accounting_date: data['date'] = self.accounting_date new_journal = self.env['account.move'].create(data) new_journal.post() self.state = 'done' def action_view_journal_price_adjustment(self): """To open `account.move (journal entries)`. Returns: Dict -- Action result. """ if len(self.journal_ids) == 1: return { 'name': 'Journal Entry', 'view_type': 'form', 'view_mode': 'form', 'res_id': self.journal_ids[0].id, 'type': 'ir.actions.act_window', 'res_model': 'account.move', 'target': 'current', } else: action = self.env.ref('account.action_move_journal_line') result = action.read()[0] result['domain'] = [('price_adjustment_id', '=', self.id)] result['context'] = {'default_price_adjustment_id': self.id} return result
class PurchaseOrderLine(models.Model): _name = 'purchase.order.line' _description = 'Purchase Order Line' _order = 'order_id, sequence, id' name = fields.Text(string='Description', required=True) sequence = fields.Integer(string='Sequence', default=10) product_qty = fields.Float( string='Quantity', digits=dp.get_precision('Product Unit of Measure'), required=True) product_uom_qty = fields.Float(string='Total Quantity', compute='_compute_product_uom_qty', store=True) date_planned = fields.Datetime(string='Scheduled Date', required=True, index=True) taxes_id = fields.Many2many( 'account.tax', string='Taxes', domain=['|', ('active', '=', False), ('active', '=', True)]) product_uom = fields.Many2one('uom.uom', string='Product Unit of Measure', required=True) product_id = fields.Many2one('product.product', string='Product', domain=[('purchase_ok', '=', True)], change_default=True, required=True) product_image = fields.Binary( 'Product Image', related="product_id.image", readonly=False, help= "Non-stored related field to allow portal user to see the image of the product he has ordered" ) product_type = fields.Selection(related='product_id.type', readonly=True) price_unit = fields.Float(string='Unit Price', required=True, digits=dp.get_precision('Product Price')) price_subtotal = fields.Monetary(compute='_compute_amount', string='Subtotal', store=True) price_total = fields.Monetary(compute='_compute_amount', string='Total', store=True) price_tax = fields.Float(compute='_compute_amount', string='Tax', store=True) order_id = fields.Many2one('purchase.order', string='Order Reference', index=True, required=True, ondelete='cascade') account_analytic_id = fields.Many2one('account.analytic.account', string='Analytic Account') analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags') company_id = fields.Many2one('res.company', related='order_id.company_id', string='Company', store=True, readonly=True) state = fields.Selection(related='order_id.state', store=True, readonly=False) invoice_lines = fields.One2many('account.invoice.line', 'purchase_line_id', string="Bill Lines", readonly=True, copy=False) # Replace by invoiced Qty qty_invoiced = fields.Float( compute='_compute_qty_invoiced', string="Billed Qty", digits=dp.get_precision('Product Unit of Measure'), store=True) qty_received = fields.Float( string="Received Qty", digits=dp.get_precision('Product Unit of Measure'), copy=False) partner_id = fields.Many2one('res.partner', related='order_id.partner_id', string='Partner', readonly=True, store=True) currency_id = fields.Many2one(related='order_id.currency_id', store=True, string='Currency', readonly=True) date_order = fields.Datetime(related='order_id.date_order', string='Order Date', readonly=True) @api.depends('product_qty', 'price_unit', 'taxes_id') def _compute_amount(self): for line in self: vals = line._prepare_compute_all_values() taxes = line.taxes_id.compute_all(vals['price_unit'], vals['currency_id'], vals['product_qty'], vals['product'], vals['partner']) line.update({ 'price_tax': sum(t.get('amount', 0.0) for t in taxes.get('taxes', [])), 'price_total': taxes['total_included'], 'price_subtotal': taxes['total_excluded'], }) def _prepare_compute_all_values(self): # Hook method to returns the different argument values for the # compute_all method, due to the fact that discounts mechanism # is not implemented yet on the purchase orders. # This method should disappear as soon as this feature is # also introduced like in the sales module. self.ensure_one() return { 'price_unit': self.price_unit, 'currency_id': self.order_id.currency_id, 'product_qty': self.product_qty, 'product': self.product_id, 'partner': self.order_id.partner_id, } @api.multi def _compute_tax_id(self): for line in self: fpos = line.order_id.fiscal_position_id or line.order_id.partner_id.property_account_position_id # If company_id is set, always filter taxes by the company taxes = line.product_id.supplier_taxes_id.filtered( lambda r: not line.company_id or r.company_id == line. company_id) line.taxes_id = fpos.map_tax( taxes, line.product_id, line.order_id.partner_id) if fpos else taxes @api.depends('invoice_lines.invoice_id.state', 'invoice_lines.quantity') def _compute_qty_invoiced(self): for line in self: qty = 0.0 for inv_line in line.invoice_lines: if inv_line.invoice_id.state not in ['cancel']: if inv_line.invoice_id.type == 'in_invoice': qty += inv_line.uom_id._compute_quantity( inv_line.quantity, line.product_uom) elif inv_line.invoice_id.type == 'in_refund': qty -= inv_line.uom_id._compute_quantity( inv_line.quantity, line.product_uom) line.qty_invoiced = qty @api.model def create(self, values): line = super(PurchaseOrderLine, self).create(values) if line.order_id.state == 'purchase': msg = _("Extra line with %s ") % (line.product_id.display_name, ) line.order_id.message_post(body=msg) return line @api.multi def write(self, values): if 'product_qty' in values: for line in self: if line.order_id.state == 'purchase': line.order_id.message_post_with_view( 'purchase.track_po_line_template', values={ 'line': line, 'product_qty': values['product_qty'] }, subtype_id=self.env.ref('mail.mt_note').id) return super(PurchaseOrderLine, self).write(values) @api.multi def unlink(self): for line in self: if line.order_id.state in ['purchase', 'done']: raise UserError( _('Cannot delete a purchase order line which is in state \'%s\'.' ) % (line.state, )) return super(PurchaseOrderLine, self).unlink() @api.model def _get_date_planned(self, seller, po=False): """Return the datetime value to use as Schedule Date (``date_planned``) for PO Lines that correspond to the given product.seller_ids, when ordered at `date_order_str`. :param Model seller: used to fetch the delivery delay (if no seller is provided, the delay is 0) :param Model po: purchase.order, necessary only if the PO line is not yet attached to a PO. :rtype: datetime :return: desired Schedule Date for the PO line """ date_order = po.date_order if po else self.order_id.date_order if date_order: return date_order + relativedelta( days=seller.delay if seller else 0) else: return datetime.today() + relativedelta( days=seller.delay if seller else 0) @api.onchange('product_id') def onchange_product_id(self): result = {} if not self.product_id: return result # Reset date, price and quantity since _onchange_quantity will provide default values self.date_planned = datetime.today().strftime( DEFAULT_SERVER_DATETIME_FORMAT) self.price_unit = self.product_qty = 0.0 self.product_uom = self.product_id.uom_po_id or self.product_id.uom_id result['domain'] = { 'product_uom': [('category_id', '=', self.product_id.uom_id.category_id.id)] } product_lang = self.product_id.with_context( lang=self.partner_id.lang, partner_id=self.partner_id.id, ) self.name = product_lang.display_name if product_lang.description_purchase: self.name += '\n' + product_lang.description_purchase self._compute_tax_id() self._suggest_quantity() self._onchange_quantity() return result @api.onchange('product_id') def onchange_product_id_warning(self): if not self.product_id: return warning = {} title = False message = False product_info = self.product_id if product_info.purchase_line_warn != 'no-message': title = _("Warning for %s") % product_info.name message = product_info.purchase_line_warn_msg warning['title'] = title warning['message'] = message if product_info.purchase_line_warn == 'block': self.product_id = False return {'warning': warning} return {} @api.onchange('product_qty', 'product_uom') def _onchange_quantity(self): if not self.product_id: return params = {'order_id': self.order_id} seller = self.product_id._select_seller( partner_id=self.partner_id, quantity=self.product_qty, date=self.order_id.date_order and self.order_id.date_order.date(), uom_id=self.product_uom, params=params) if seller or not self.date_planned: self.date_planned = self._get_date_planned(seller).strftime( DEFAULT_SERVER_DATETIME_FORMAT) if not seller: if self.product_id.seller_ids.filtered( lambda s: s.name.id == self.partner_id.id): self.price_unit = 0.0 return price_unit = self.env['account.tax']._fix_tax_included_price_company( seller.price, self.product_id.supplier_taxes_id, self.taxes_id, self.company_id) if seller else 0.0 if price_unit and seller and self.order_id.currency_id and seller.currency_id != self.order_id.currency_id: price_unit = seller.currency_id._convert( price_unit, self.order_id.currency_id, self.order_id.company_id, self.date_order or fields.Date.today()) if seller and self.product_uom and seller.product_uom != self.product_uom: price_unit = seller.product_uom._compute_price( price_unit, self.product_uom) self.price_unit = price_unit @api.multi @api.depends('product_uom', 'product_qty', 'product_id.uom_id') def _compute_product_uom_qty(self): for line in self: if line.product_id.uom_id != line.product_uom: line.product_uom_qty = line.product_uom._compute_quantity( line.product_qty, line.product_id.uom_id) else: line.product_uom_qty = line.product_qty def _suggest_quantity(self): ''' Suggest a minimal quantity based on the seller ''' if not self.product_id: return seller_min_qty = self.product_id.seller_ids\ .filtered(lambda r: r.name == self.order_id.partner_id and (not r.product_id or r.product_id == self.product_id))\ .sorted(key=lambda r: r.min_qty) if seller_min_qty: self.product_qty = seller_min_qty[0].min_qty or 1.0 self.product_uom = seller_min_qty[0].product_uom else: self.product_qty = 1.0
class Job(models.Model): _name = "hr.job" _description = "Job Position" _inherit = ['mail.thread'] def _default_groups(self): default_user = self.env.ref('base.default_user', raise_if_not_found=False) return (default_user or self.env['res.users']).sudo().groups_id name = fields.Char(string='Job Title', required=True, index=True, translate=True) expected_employees = fields.Integer( compute='_compute_employees', string='Total Forecasted Employees', store=True, help= 'Expected number of employees for this job position after new recruitment.' ) no_of_employee = fields.Integer( compute='_compute_employees', string="Current Number of Employees", store=True, help='Number of employees currently occupying this job position.') no_of_recruitment = fields.Integer( string='Expected New Employees', copy=False, help='Number of new employees you expect to recruit.', default=1) no_of_hired_employee = fields.Integer( string='Hired Employees', copy=False, help= 'Number of hired employees for this job position during recruitment phase.' ) employee_ids = fields.One2many('hr.employee', 'job_id', string='Employees', groups='base.group_user') description = fields.Text(string='Job Description') requirements = fields.Text('Requirements') department_id = fields.Many2one('hr.department', string='Department') company_id = fields.Many2one('res.company', string='Company', default=lambda self: self.env.user.company_id) state = fields.Selection( [('recruit', 'Recruitment in Progress'), ('open', 'Not Recruiting')], string='Status', readonly=True, required=True, track_visibility='always', copy=False, default='recruit', help= "Set whether the recruitment process is open or closed for this job position." ) groups_id = fields.Many2many('res.groups', 'res_groups_users_rel', 'uid', 'gid', string='Groups', default=_default_groups) _sql_constraints = [ ('name_company_uniq', 'unique(name, company_id, department_id)', 'The name of the job position must be unique per department in company!' ), ] @api.depends('no_of_recruitment', 'employee_ids.job_id', 'employee_ids.active') def _compute_employees(self): employee_data = self.env['hr.employee'].read_group( [('job_id', 'in', self.ids)], ['job_id'], ['job_id']) result = dict((data['job_id'][0], data['job_id_count']) for data in employee_data) for job in self: job.no_of_employee = result.get(job.id, 0) job.expected_employees = result.get(job.id, 0) + job.no_of_recruitment @api.model def create(self, values): """ We don't want the current user to be follower of all created job """ return super( Job, self.with_context(mail_create_nosubscribe=True)).create(values) @api.multi def copy(self, default=None): self.ensure_one() default = dict(default or {}) if 'name' not in default: default['name'] = _("%s (copy)") % (self.name) return super(Job, self).copy(default=default) @api.multi def set_recruit(self): for record in self: no_of_recruitment = 1 if record.no_of_recruitment == 0 else record.no_of_recruitment record.write({ 'state': 'recruit', 'no_of_recruitment': no_of_recruitment }) return True @api.multi def set_open(self): return self.write({ 'state': 'open', 'no_of_recruitment': 0, 'no_of_hired_employee': 0 })
class ProductProduct(models.Model): _name = "product.product" _description = "Product" _inherits = {'product.template': 'product_tmpl_id'} _inherit = ['mail.thread', 'mail.activity.mixin'] _order = 'default_code, name, id' price = fields.Float('Price', compute='_compute_product_price', digits=dp.get_precision('Product Price'), inverse='_set_product_price') price_extra = fields.Float( 'Variant Price Extra', compute='_compute_product_price_extra', digits=dp.get_precision('Product Price'), help="This is the sum of the extra price of all attributes") lst_price = fields.Float( 'Sale Price', compute='_compute_product_lst_price', digits=dp.get_precision('Product Price'), inverse='_set_product_lst_price', help= "The sale price is managed from the product template. Click on the 'Variant Prices' button to set the extra attribute prices." ) default_code = fields.Char('Internal Reference', index=True) code = fields.Char('Reference', compute='_compute_product_code') partner_ref = fields.Char('Customer Ref', compute='_compute_partner_ref') active = fields.Boolean( 'Active', default=True, help= "If unchecked, it will allow you to hide the product without removing it." ) product_tmpl_id = fields.Many2one('product.template', 'Product Template', auto_join=True, index=True, ondelete="cascade", required=True) barcode = fields.Char( 'Barcode', copy=False, oldname='ean13', help="International Article Number used for product identification.") attribute_value_ids = fields.Many2many('product.attribute.value', string='Attributes', ondelete='restrict') # image: all image fields are base64 encoded and PIL-supported image_variant = fields.Binary( "Variant Image", attachment=True, help= "This field holds the image used as image for the product variant, limited to 1024x1024px." ) image = fields.Binary( "Big-sized image", compute='_compute_images', inverse='_set_image', help= "Image of the product variant (Big-sized image of product template if false). It is automatically " "resized as a 1024x1024px image, with aspect ratio preserved.") image_small = fields.Binary( "Small-sized image", compute='_compute_images', inverse='_set_image_small', help= "Image of the product variant (Small-sized image of product template if false)." ) image_medium = fields.Binary( "Medium-sized image", compute='_compute_images', inverse='_set_image_medium', help= "Image of the product variant (Medium-sized image of product template if false)." ) standard_price = fields.Float( 'Cost', company_dependent=True, digits=dp.get_precision('Product Price'), groups="base.group_user", help= "Cost used for stock valuation in standard price and as a first price to set in average/fifo. " "Also used as a base price for pricelists. " "Expressed in the default unit of measure of the product.") volume = fields.Float('Volume', help="The volume in m3.") weight = fields.Float( 'Weight', digits=dp.get_precision('Stock Weight'), help= "The weight of the contents in Kg, not including any packaging, etc.") pricelist_item_ids = fields.Many2many('product.pricelist.item', 'Pricelist Items', compute='_get_pricelist_items') packaging_ids = fields.One2many( 'product.packaging', 'product_id', 'Product Packages', help="Gives the different ways to package the same product.") _sql_constraints = [ ('barcode_uniq', 'unique(barcode)', "A barcode can only be assigned to one product !"), ] def _get_invoice_policy(self): return False def _compute_product_price(self): prices = {} pricelist_id_or_name = self._context.get('pricelist') if pricelist_id_or_name: pricelist = None partner = self._context.get('partner', False) quantity = self._context.get('quantity', 1.0) # Support context pricelists specified as display_name or ID for compatibility if isinstance(pricelist_id_or_name, pycompat.string_types): pricelist_name_search = self.env[ 'product.pricelist'].name_search(pricelist_id_or_name, operator='=', limit=1) if pricelist_name_search: pricelist = self.env['product.pricelist'].browse( [pricelist_name_search[0][0]]) elif isinstance(pricelist_id_or_name, pycompat.integer_types): pricelist = self.env['product.pricelist'].browse( pricelist_id_or_name) if pricelist: quantities = [quantity] * len(self) partners = [partner] * len(self) prices = pricelist.get_products_price(self, quantities, partners) for product in self: product.price = prices.get(product.id, 0.0) def _set_product_price(self): for product in self: if self._context.get('uom'): value = self.env['product.uom'].browse( self._context['uom'])._compute_price( product.price, product.uom_id) else: value = product.price value -= product.price_extra product.write({'list_price': value}) def _set_product_lst_price(self): for product in self: if self._context.get('uom'): value = self.env['product.uom'].browse( self._context['uom'])._compute_price( product.lst_price, product.uom_id) else: value = product.lst_price value -= product.price_extra product.write({'list_price': value}) @api.depends('attribute_value_ids.price_ids.price_extra', 'attribute_value_ids.price_ids.product_tmpl_id') def _compute_product_price_extra(self): # TDE FIXME: do a real multi and optimize a bit ? for product in self: price_extra = 0.0 for attribute_price in product.mapped( 'attribute_value_ids.price_ids'): if attribute_price.product_tmpl_id == product.product_tmpl_id: price_extra += attribute_price.price_extra product.price_extra = price_extra @api.depends('list_price', 'price_extra') def _compute_product_lst_price(self): to_uom = None if 'uom' in self._context: to_uom = self.env['product.uom'].browse([self._context['uom']]) for product in self: if to_uom: list_price = product.uom_id._compute_price( product.list_price, to_uom) else: list_price = product.list_price product.lst_price = list_price + product.price_extra @api.one def _compute_product_code(self): for supplier_info in self.seller_ids: if supplier_info.name.id == self._context.get('partner_id'): self.code = supplier_info.product_code or self.default_code else: self.code = self.default_code @api.one def _compute_partner_ref(self): for supplier_info in self.seller_ids: if supplier_info.name.id == self._context.get('partner_id'): product_name = supplier_info.product_name or self.default_code self.partner_ref = '%s%s' % (self.code and '[%s] ' % self.code or '', product_name) else: self.partner_ref = self.name_get()[0][1] @api.one @api.depends('image_variant', 'product_tmpl_id.image') def _compute_images(self): if self._context.get('bin_size'): self.image_medium = self.image_variant self.image_small = self.image_variant self.image = self.image_variant else: resized_images = tools.image_get_resized_images( self.image_variant, return_big=True, avoid_resize_medium=True) self.image_medium = resized_images['image_medium'] self.image_small = resized_images['image_small'] self.image = resized_images['image'] if not self.image_medium: self.image_medium = self.product_tmpl_id.image_medium if not self.image_small: self.image_small = self.product_tmpl_id.image_small if not self.image: self.image = self.product_tmpl_id.image @api.one def _set_image(self): self._set_image_value(self.image) @api.one def _set_image_medium(self): self._set_image_value(self.image_medium) @api.one def _set_image_small(self): self._set_image_value(self.image_small) @api.one def _set_image_value(self, value): if isinstance(value, pycompat.text_type): value = value.encode('ascii') image = tools.image_resize_image_big(value) if self.product_tmpl_id.image: self.image_variant = image else: self.product_tmpl_id.image = image @api.one def _get_pricelist_items(self): self.pricelist_item_ids = self.env['product.pricelist.item'].search([ '|', ('product_id', '=', self.id), ('product_tmpl_id', '=', self.product_tmpl_id.id) ]).ids @api.constrains('attribute_value_ids') def _check_attribute_value_ids(self): for product in self: attributes = self.env['product.attribute'] for value in product.attribute_value_ids: if value.attribute_id in attributes: raise ValidationError( _('Error! It is not allowed to choose more than one value for a given attribute.' )) if value.attribute_id.create_variant: attributes |= value.attribute_id return True @api.onchange('uom_id', 'uom_po_id') def _onchange_uom(self): if self.uom_id and self.uom_po_id and self.uom_id.category_id != self.uom_po_id.category_id: self.uom_po_id = self.uom_id @api.model def create(self, vals): product = super( ProductProduct, self.with_context(create_product_product=True)).create(vals) # When a unique variant is created from tmpl then the standard price is set by _set_standard_price if not (self.env.context.get('create_from_tmpl') and len(product.product_tmpl_id.product_variant_ids) == 1): product._set_standard_price(vals.get('standard_price') or 0.0) return product @api.multi def write(self, values): ''' Store the standard price change in order to be able to retrieve the cost of a product for a given date''' res = super(ProductProduct, self).write(values) if 'standard_price' in values: self._set_standard_price(values['standard_price']) return res @api.multi def unlink(self): unlink_products = self.env['product.product'] unlink_templates = self.env['product.template'] for product in self: # Check if product still exists, in case it has been unlinked by unlinking its template if not product.exists(): continue # Check if the product is last product of this template other_products = self.search([('product_tmpl_id', '=', product.product_tmpl_id.id), ('id', '!=', product.id)]) if not other_products: unlink_templates |= product.product_tmpl_id unlink_products |= product res = super(ProductProduct, unlink_products).unlink() # delete templates after calling super, as deleting template could lead to deleting # products due to ondelete='cascade' unlink_templates.unlink() return res @api.multi def copy(self, default=None): # TDE FIXME: clean context / variant brol if default is None: default = {} if self._context.get('variant'): # if we copy a variant or create one, we keep the same template default['product_tmpl_id'] = self.product_tmpl_id.id elif 'name' not in default: default['name'] = self.name return super(ProductProduct, self).copy(default=default) @api.model def search(self, args, offset=0, limit=None, order=None, count=False): # TDE FIXME: strange if self._context.get('search_default_categ_id'): args.append((('categ_id', 'child_of', self._context['search_default_categ_id']))) return super(ProductProduct, self).search(args, offset=offset, limit=limit, order=order, count=count) @api.multi def name_get(self): # TDE: this could be cleaned a bit I think def _name_get(d): name = d.get('name', '') code = self._context.get('display_default_code', True) and d.get( 'default_code', False) or False if code: name = '[%s] %s' % (code, name) return (d['id'], name) partner_id = self._context.get('partner_id') if partner_id: partner_ids = [ partner_id, self.env['res.partner'].browse( partner_id).commercial_partner_id.id ] else: partner_ids = [] # all user don't have access to seller and partner # check access and use superuser self.check_access_rights("read") self.check_access_rule("read") result = [] for product in self.sudo(): # display only the attributes with multiple possible values on the template variable_attributes = product.attribute_line_ids.filtered( lambda l: len(l.value_ids) > 1).mapped('attribute_id') variant = product.attribute_value_ids._variant_name( variable_attributes) name = variant and "%s (%s)" % (product.name, variant) or product.name sellers = [] if partner_ids: sellers = [ x for x in product.seller_ids if (x.name.id in partner_ids) and (x.product_id == product) ] if not sellers: sellers = [ x for x in product.seller_ids if (x.name.id in partner_ids) and not x.product_id ] if sellers: for s in sellers: seller_variant = s.product_name and ( variant and "%s (%s)" % (s.product_name, variant) or s.product_name) or False mydict = { 'id': product.id, 'name': seller_variant or name, 'default_code': s.product_code or product.default_code, } temp = _name_get(mydict) if temp not in result: result.append(temp) else: mydict = { 'id': product.id, 'name': name, 'default_code': product.default_code, } result.append(_name_get(mydict)) return result @api.model def name_search(self, name='', args=None, operator='ilike', limit=100): if not args: args = [] if name: positive_operators = ['=', 'ilike', '=ilike', 'like', '=like'] products = self.env['product.product'] if operator in positive_operators: products = self.search([('default_code', '=', name)] + args, limit=limit) if not products: products = self.search([('barcode', '=', name)] + args, limit=limit) if not products and operator not in expression.NEGATIVE_TERM_OPERATORS: # Do not merge the 2 next lines into one single search, SQL search performance would be abysmal # on a database with thousands of matching products, due to the huge merge+unique needed for the # OR operator (and given the fact that the 'name' lookup results come from the ir.translation table # Performing a quick memory merge of ids in Python will give much better performance products = self.search(args + [('default_code', operator, name)], limit=limit) if not limit or len(products) < limit: # we may underrun the limit because of dupes in the results, that's fine limit2 = (limit - len(products)) if limit else False products += self.search(args + [('name', operator, name), ('id', 'not in', products.ids)], limit=limit2) elif not products and operator in expression.NEGATIVE_TERM_OPERATORS: domain = expression.OR([ [ '&', ('default_code', operator, name), ('name', operator, name) ], [ '&', ('default_code', '=', False), ('name', operator, name) ], ]) domain = expression.AND([args, domain]) products = self.search(domain, limit=limit) if not products and operator in positive_operators: ptrn = re.compile('(\[(.*?)\])') res = ptrn.search(name) if res: products = self.search( [('default_code', '=', res.group(2))] + args, limit=limit) # still no results, partner in context: search on supplier info as last hope to find something if not products and self._context.get('partner_id'): suppliers = self.env['product.supplierinfo'].search([ ('name', '=', self._context.get('partner_id')), '|', ('product_code', operator, name), ('product_name', operator, name) ]) if suppliers: products = self.search( [('product_tmpl_id.seller_ids', 'in', suppliers.ids)], limit=limit) else: products = self.search(args, limit=limit) return products.name_get() @api.model def view_header_get(self, view_id, view_type): res = super(ProductProduct, self).view_header_get(view_id, view_type) if self._context.get('categ_id'): return _('Products: ') + self.env['product.category'].browse( self._context['categ_id']).name return res @api.multi def open_product_template(self): """ Utility method used to add an "Open Template" button in product views """ self.ensure_one() return { 'type': 'ir.actions.act_window', 'res_model': 'product.template', 'view_mode': 'form', 'res_id': self.product_tmpl_id.id, 'target': 'new' } @api.multi def _select_seller(self, partner_id=False, quantity=0.0, date=None, uom_id=False): self.ensure_one() if date is None: date = fields.Date.context_today(self) precision = self.env['decimal.precision'].precision_get( 'Product Unit of Measure') res = self.env['product.supplierinfo'] for seller in self.seller_ids: # Set quantity in UoM of seller quantity_uom_seller = quantity if quantity_uom_seller and uom_id and uom_id != seller.product_uom: quantity_uom_seller = uom_id._compute_quantity( quantity_uom_seller, seller.product_uom) if seller.date_start and seller.date_start > date: continue if seller.date_end and seller.date_end < date: continue if partner_id and seller.name not in [ partner_id, partner_id.parent_id ]: continue if float_compare(quantity_uom_seller, seller.min_qty, precision_digits=precision) == -1: continue if seller.product_id and seller.product_id != self: continue res |= seller break return res @api.multi def price_compute(self, price_type, uom=False, currency=False, company=False): # TDE FIXME: delegate to template or not ? fields are reencoded here ... # compatibility about context keys used a bit everywhere in the code if not uom and self._context.get('uom'): uom = self.env['product.uom'].browse(self._context['uom']) if not currency and self._context.get('currency'): currency = self.env['res.currency'].browse( self._context['currency']) products = self if price_type == 'standard_price': # standard_price field can only be seen by users in base.group_user # Thus, in order to compute the sale price from the cost for users not in this group # We fetch the standard price as the superuser products = self.with_context( force_company=company and company.id or self._context.get( 'force_company', self.env.user.company_id.id)).sudo() prices = dict.fromkeys(self.ids, 0.0) for product in products: prices[product.id] = product[price_type] or 0.0 if price_type == 'list_price': prices[product.id] += product.price_extra if uom: prices[product.id] = product.uom_id._compute_price( prices[product.id], uom) # Convert from current user company currency to asked one # This is right cause a field cannot be in more than one currency if currency: prices[product.id] = product.currency_id.compute( prices[product.id], currency) return prices # compatibility to remove after v10 - DEPRECATED @api.multi def price_get(self, ptype='list_price'): return self.price_compute(ptype) @api.multi def _set_standard_price(self, value): ''' Store the standard price change in order to be able to retrieve the cost of a product for a given date''' PriceHistory = self.env['product.price.history'] for product in self: PriceHistory.create({ 'product_id': product.id, 'cost': value, 'company_id': self._context.get('force_company', self.env.user.company_id.id), }) @api.multi def get_history_price(self, company_id, date=None): history = self.env['product.price.history'].search( [('company_id', '=', company_id), ('product_id', 'in', self.ids), ('datetime', '<=', date or fields.Datetime.now())], order='datetime desc,id desc', limit=1) return history.cost or 0.0
class SaleOrder(models.Model): _inherit = "sale.order" @api.model def _default_warehouse_id(self): company = self.env.user.company_id.id warehouse_ids = self.env['stock.warehouse'].search( [('company_id', '=', company)], limit=1) return warehouse_ids incoterm = fields.Many2one( 'stock.incoterms', 'Incoterms', help= "International Commercial Terms are a series of predefined commercial terms used in international transactions." ) picking_policy = fields.Selection( [('direct', 'Deliver each product when available'), ('one', 'Deliver all products at once')], string='Shipping Policy', required=True, readonly=True, default='direct', states={ 'draft': [('readonly', False)], 'sent': [('readonly', False)] }, help= "If you deliver all products at once, the delivery order will be scheduled based on the greatest " "product lead time. Otherwise, it will be based on the shortest.") warehouse_id = fields.Many2one('stock.warehouse', string='Warehouse', required=True, readonly=True, states={ 'draft': [('readonly', False)], 'sent': [('readonly', False)] }, default=_default_warehouse_id) picking_ids = fields.One2many('stock.picking', 'sale_id', string='Pickings') delivery_count = fields.Integer(string='Delivery Orders', compute='_compute_picking_ids') procurement_group_id = fields.Many2one('procurement.group', 'Procurement Group', copy=False) @api.multi def write(self, values): if values.get('order_line') and self.state == 'sale': for order in self: pre_order_line_qty = { order_line: order_line.product_uom_qty for order_line in order.mapped('order_line') } res = super(SaleOrder, self).write(values) if values.get('order_line') and self.state == 'sale': for order in self: to_log = {} for order_line in order.order_line: if pre_order_line_qty.get( order_line, False) and float_compare( order_line.product_uom_qty, pre_order_line_qty[order_line], order_line.product_uom.rounding) < 0: to_log[order_line] = (order_line.product_uom_qty, pre_order_line_qty[order_line]) if to_log: documents = self.env[ 'stock.picking']._log_activity_get_documents( to_log, 'move_ids', 'UP') order._log_decrease_ordered_quantity(documents) return res @api.multi def _action_confirm(self): super(SaleOrder, self)._action_confirm() for order in self: order.order_line._action_launch_stock_rule() @api.depends('picking_ids') def _compute_picking_ids(self): for order in self: order.delivery_count = len(order.picking_ids) @api.onchange('warehouse_id') def _onchange_warehouse_id(self): if self.warehouse_id.company_id: self.company_id = self.warehouse_id.company_id.id @api.multi def action_view_delivery(self): ''' This function returns an action that display existing delivery orders of given sales order ids. It can either be a in a list or in a form view, if there is only one delivery order to show. ''' action = self.env.ref('stock.action_picking_tree_all').read()[0] pickings = self.mapped('picking_ids') if len(pickings) > 1: action['domain'] = [('id', 'in', pickings.ids)] elif pickings: action['views'] = [(self.env.ref('stock.view_picking_form').id, 'form')] action['res_id'] = pickings.id return action @api.multi def action_cancel(self): documents = None for sale_order in self: if sale_order.state == 'sale' and sale_order.order_line: sale_order_lines_quantities = { order_line: (order_line.product_uom_qty, 0) for order_line in sale_order.order_line } documents = self.env[ 'stock.picking']._log_activity_get_documents( sale_order_lines_quantities, 'move_ids', 'UP') self.mapped('picking_ids').action_cancel() if documents: filtered_documents = {} for (parent, responsible), rendering_context in documents.items(): if parent._name == 'stock.picking': if parent.state == 'cancel': continue filtered_documents[(parent, responsible)] = rendering_context self._log_decrease_ordered_quantity(filtered_documents, cancel=True) return super(SaleOrder, self).action_cancel() @api.multi def _prepare_invoice(self): invoice_vals = super(SaleOrder, self)._prepare_invoice() invoice_vals['incoterms_id'] = self.incoterm.id or False return invoice_vals @api.model def _get_customer_lead(self, product_tmpl_id): super(SaleOrder, self)._get_customer_lead(product_tmpl_id) return product_tmpl_id.sale_delay def _log_decrease_ordered_quantity(self, documents, cancel=False): def _render_note_exception_quantity_so(rendering_context): order_exceptions, visited_moves = rendering_context visited_moves = list(visited_moves) visited_moves = self.env[visited_moves[0]._name].concat( *visited_moves) order_line_ids = self.env['sale.order.line'].browse([ order_line.id for order in order_exceptions.values() for order_line in order[0] ]) sale_order_ids = order_line_ids.mapped('order_id') impacted_pickings = visited_moves.filtered( lambda m: m.state not in ('done', 'cancel')).mapped( 'picking_id') values = { 'sale_order_ids': sale_order_ids, 'order_exceptions': order_exceptions.values(), 'impacted_pickings': impacted_pickings, 'cancel': cancel } return self.env.ref('sale_stock.exception_on_so').render( values=values) self.env['stock.picking']._log_activity( _render_note_exception_quantity_so, documents)
class SchoolStandard(models.Model): ''' Defining a standard related to school ''' _name = 'school.standard' _description = 'School Standards' _rec_name = "standard_id" @api.depends('standard_id', 'school_id', 'division_id', 'medium_id', 'school_id') def _compute_student(self): '''Compute student of done state''' student_obj = self.env['student.student'] for rec in self: rec.student_ids = student_obj.\ search([('standard_id', '=', rec.id), ('school_id', '=', rec.school_id.id), ('division_id', '=', rec.division_id.id), ('medium_id', '=', rec.medium_id.id), ('state', '=', 'done')]) @api.onchange('standard_id', 'division_id') def onchange_combine(self): self.name = str(self.standard_id.name ) + '-' + str(self.division_id.name) @api.depends('subject_ids') def _compute_subject(self): '''Method to compute subjects''' for rec in self: rec.total_no_subjects = len(rec.subject_ids) @api.depends('student_ids') def _compute_total_student(self): for rec in self: rec.total_students = len(rec.student_ids) @api.depends("capacity", "total_students") def _compute_remain_seats(self): for rec in self: rec.remaining_seats = rec.capacity - rec.total_students school_id = fields.Many2one('school.school', 'School', required=True) standard_id = fields.Many2one('standard.standard', 'Standard', required=True) division_id = fields.Many2one('standard.division', 'Division', required=True) medium_id = fields.Many2one('standard.medium', 'Medium', required=True) subject_ids = fields.Many2many('subject.subject', 'subject_standards_rel', 'subject_id', 'standard_id', 'Subject') user_id = fields.Many2one('school.teacher', 'Class Teacher') student_ids = fields.One2many('student.student', 'standard_id', 'Student In Class', compute='_compute_student', store=True ) color = fields.Integer('Color Index') cmp_id = fields.Many2one('res.company', 'Company Name', related='school_id.company_id', store=True) syllabus_ids = fields.One2many('subject.syllabus', 'standard_id', 'Syllabus') total_no_subjects = fields.Integer('Total No of Subject', compute="_compute_subject") name = fields.Char('Name') capacity = fields.Integer("Total Seats") total_students = fields.Integer("Total Students", compute="_compute_total_student", store=True) remaining_seats = fields.Integer("Available Seats", compute="_compute_remain_seats", store=True) class_room_id = fields.Many2one('class.room', 'Room Number') @api.constrains('standard_id', 'division_id') def check_standard_unique(self): standard_search = self.env['school.standard' ].search([('standard_id', '=', self.standard_id.id), ('division_id', '=', self.division_id.id), ('school_id', '=', self.school_id.id), ('id', 'not in', self.ids)]) if standard_search: raise ValidationError(_('''Division and class should be unique!''' )) @api.multi def unlink(self): for rec in self: if rec.student_ids or rec.subject_ids or rec.syllabus_ids: raise ValidationError(_('''You cannot delete this standard because it has reference with student or subject or syllabus!''')) return super(SchoolStandard, self).unlink() @api.constrains('capacity') def check_seats(self): if self.capacity <= 0: raise ValidationError(_('''Total seats should be greater than 0!''')) @api.multi def name_get(self): '''Method to display standard and division''' return [(rec.id, rec.standard_id.name + '[' + rec.division_id.name + ']') for rec in self]