def attributes(self, record, field_name, options, values): attrs = super(Date, self).attributes(record, field_name, options, values) if options.get('inherit_branding'): attrs['data-oe-original'] = record[field_name] if record._fields[field_name].type == 'datetime': attrs = self.env['ir.qweb.field.datetime'].attributes(record, field_name, options, values) attrs['data-oe-type'] = 'datetime' return attrs lg = self.env['res.lang']._lang_get(self.env.user.lang) or get_lang(self.env) locale = babel.Locale.parse(lg.code) babel_format = value_format = posix_to_ldml(lg.date_format, locale=locale) if record[field_name]: date = fields.Date.from_string(record[field_name]) value_format = pycompat.to_text(babel.dates.format_date(date, format=babel_format, locale=locale)) attrs['data-oe-original-with-format'] = value_format return attrs
def get_html(self, options, line_id=None, additional_context=None): """ Override Compute and return the content in HTML of the followup for the partner_id in options """ if additional_context is None: additional_context = {} additional_context['followup_line'] = self.get_followup_line( options) partner = self.env['res.partner'].browse(options['partner_id']) additional_context['partner'] = partner additional_context['lang'] = partner.lang or get_lang(self.env).code additional_context['invoice_address_id'] = self.env[ 'res.partner'].browse(partner.address_get(['invoice'])['invoice']) additional_context['today'] = fields.date.today().strftime( DEFAULT_SERVER_DATE_FORMAT) return super(AccountFollowupReport, self).get_html(options, line_id=line_id, additional_context=additional_context)
def calendar_appointment_view(self, access_token, edit=False, message=False, **kwargs): event = request.env['calendar.event'].sudo().search([('access_token', '=', access_token)], limit=1) if not event: return request.not_found() timezone = request.session.get('timezone') if not timezone: timezone = request.env.context.get('tz') or event.appointment_type_id.appointment_tz or event.partner_ids and event.partner_ids[0].tz request.session['timezone'] = timezone tz_session = pytz.timezone(timezone) date_start_suffix = "" format_func = format_datetime if not event.allday: url_date_start = fields.Datetime.from_string(event.start_datetime).strftime('%Y%m%dT%H%M%SZ') url_date_stop = fields.Datetime.from_string(event.stop_datetime).strftime('%Y%m%dT%H%M%SZ') date_start = fields.Datetime.from_string(event.start_datetime).replace(tzinfo=pytz.utc).astimezone(tz_session) else: url_date_start = url_date_stop = fields.Date.from_string(event.start_date).strftime('%Y%m%d') date_start = fields.Date.from_string(event.start_date) format_func = format_date date_start_suffix = _(', All Day') locale = get_lang(request.env).code day_name = format_func(date_start, 'EEE', locale=locale) date_start = day_name + ' ' + format_func(date_start, locale=locale) + date_start_suffix details = event.appointment_type_id and event.appointment_type_id.message_confirmation or event.description or '' params = url_encode({ 'action': 'TEMPLATE', 'text': event.name, 'dates': url_date_start + '/' + url_date_stop, 'details': html2plaintext(details.encode('utf-8')) }) google_url = 'https://www.google.com/calendar/render?' + params return request.render("website_calendar.appointment_validated", { 'event': event, 'datetime_start': date_start, 'google_url': google_url, 'message': message, 'edit': edit, })
def open_rating(self, token, rate, **kwargs): assert rate in (1, 5, 10), "Incorrect rating" rating = request.env['rating.rating'].sudo().search([('access_token', '=', token)]) if not rating: return request.not_found() rate_names = { 5: _("not satisfied"), 1: _("highly dissatisfied"), 10: _("satisfied") } rating.write({'rating': rate, 'consumed': True}) lang = rating.partner_id.lang or get_lang(request.env).code return request.env['ir.ui.view'].with_context( lang=lang).render_template( 'rating.rating_external_page_submit', { 'rating': rating, 'token': token, 'rate_name': rate_names[rate], 'rate': rate })
def send_and_print_action(self): self.ensure_one() # Send the mails in the correct language by splitting the ids per lang. # This should ideally be fixed in mail_compose_message, so when a fix is made there this whole commit should be reverted. # basically self.body (which could be manually edited) extracts self.template_id, # which is then not translated for each customer. if self.composition_mode == 'mass_mail' and self.template_id: active_ids = self.env.context.get('active_ids', self.res_id) active_records = self.env[self.model].browse(active_ids) langs = active_records.mapped('partner_id.lang') default_lang = get_lang(self.env) for lang in (set(langs) or [default_lang]): active_ids_lang = active_records.filtered(lambda r: r.partner_id.lang == lang).ids self_lang = self.with_context(active_ids=active_ids_lang, lang=lang) self_lang.onchange_template_id() self_lang._send_email() else: self._send_email() if self.is_print: return self._print_document() return {'type': 'ir.actions.act_window_close'}
def action_compute_pricelist_discount(self): """ Compute pricelist discounts on current sale order This action is only possible in draft state to prevent side effects """ self.ensure_one() if self.state in ['draft', 'sent']: for line in self.order_line: if self.pricelist_id.discount_policy == 'with_discount': line.discount = 0.0 product = line.product_id.with_context( lang=get_lang(self.env, line.order_id.partner_id.lang).code, partner=line.order_id.partner_id, quantity=line.product_uom_qty, date=line.order_id.date_order, pricelist=line.order_id.pricelist_id.id, uom=line.product_uom.id ) if line.order_id.pricelist_id and line.order_id.partner_id: line.price_unit = self.env['account.tax']._fix_tax_included_price_company(line._get_display_price(product), product.taxes_id, line.tax_id, line.company_id) line._onchange_discount()
def _onchange_product_id(self): if not self.product_id: return # valid_values = self.product_id.product_tmpl_id.valid_product_template_attribute_line_ids.product_template_value_ids # remove the is_custom values that don't belong to this template # for pacv in self.product_custom_attribute_value_ids: # if pacv.custom_product_template_attribute_value_id not in valid_values: # self.product_custom_attribute_value_ids -= pacv # # # remove the no_variant attributes that don't belong to this template # for ptav in self.product_no_variant_attribute_value_ids: # if ptav._origin not in valid_values: # self.product_no_variant_attribute_value_ids -= ptav vals = {} if not self.product_uom or (self.product_id.uom_id.id != self.product_uom.id): vals['product_uom'] = self.product_id.uom_id vals['product_uom_qty'] = self.product_uom_qty or 1.0 product = self.product_id.with_context( lang=get_lang(self.env, self.order_id.partner_id.lang).code, partner=self.order_id.partner_id, quantity=vals.get('product_uom_qty') or self.product_uom_qty, date=self.order_id.date_order, pricelist=self.order_id.pricelist_id.id, uom=self.product_uom.id ) if self.order_id.pricelist_id and self.order_id.partner_id: vals['price_unit'] = self.env['account.tax']._fix_tax_included_price_company( self._get_display_price(product), product.taxes_id, self.tax_id, self.company_id) # self.price_unit = self.product_id.list_price # self.product_uom = self.product_id.uom_id.id # self.name = self.product_id.name if not self.name: vals['name'] = self.product_id.name self.update(vals)
def discuss_channel_invitation(self, channel_id, invitation_token, **kwargs): channel_sudo = request.env['mail.channel'].browse( int(channel_id)).sudo().exists() if not channel_sudo or not channel_sudo.uuid or not consteq( channel_sudo.uuid, invitation_token): raise NotFound() response = request.redirect(f'/discuss/channel/{channel_sudo.id}') if request.env['mail.channel.partner']._get_as_sudo_from_request( request=request, channel_id=int(channel_id)): return response if channel_sudo.channel_type == 'chat': raise NotFound() if request.session.uid: channel_sudo.add_members([request.env.user.partner_id.id]) return response guest = request.env['mail.guest']._get_guest_from_request(request) if not guest: guest = request.env['mail.guest'].sudo().create({ 'country_id': request.env['res.country'].sudo().search( [('code', '=', request.session.get( 'geoip', {}).get('country_code'))], limit=1).id, 'lang': get_lang(request.env).code, 'name': _("Guest"), 'timezone': request.env['mail.guest']._get_timezone_from_request(request), }) # Guest session cookies, every route in this file will make use of them to authenticate # the guest through `_get_as_sudo_from_request` or `_get_as_sudo_from_request_or_raise`. response.set_cookie('mail.guest_id', str(guest.id), httponly=True) response.set_cookie('mail.guest_access_token', guest.access_token, httponly=True) channel_sudo.add_members(guest_ids=[guest.id]) return response
def action_submit_rating(self, token, **kwargs): rate = int(kwargs.get('rate')) assert rate in (1, 3, 5), "Incorrect rating" rating = request.env['rating.rating'].sudo().search([('access_token', '=', token)]) if not rating: return request.not_found() record_sudo = request.env[rating.res_model].sudo().browse( rating.res_id) record_sudo.rating_apply(rate, token=token, feedback=kwargs.get('feedback')) lang = rating.partner_id.lang or get_lang(request.env).code return request.env['ir.ui.view'].with_context( lang=lang).render_template( 'rating.rating_external_page_view', { 'web_base_url': request.env['ir.config_parameter'].sudo().get_param( 'web.base.url'), 'rating': rating, })
def _response_discuss_channel_invitation(self, channel_sudo, is_channel_token_secret=True): if channel_sudo.channel_type == 'chat': raise NotFound() discuss_public_view_data = { 'isChannelTokenSecret': is_channel_token_secret, } add_guest_cookie = False channel_partner_sudo = channel_sudo.env['mail.channel.partner']._get_as_sudo_from_request(request=request, channel_id=channel_sudo.id) if channel_partner_sudo: channel_sudo = channel_partner_sudo.channel_id # ensure guest is in context else: if not channel_sudo.env.user._is_public(): channel_sudo.add_members([channel_sudo.env.user.partner_id.id]) else: guest = channel_sudo.env['mail.guest']._get_guest_from_request(request) if guest: channel_sudo = channel_sudo.with_context(guest=guest) channel_sudo.add_members(guest_ids=[guest.id]) else: guest = channel_sudo.env['mail.guest'].create({ 'country_id': channel_sudo.env['res.country'].search([('code', '=', request.session.get('geoip', {}).get('country_code'))], limit=1).id, 'lang': get_lang(channel_sudo.env).code, 'name': _("Guest"), 'timezone': channel_sudo.env['mail.guest']._get_timezone_from_request(request), }) add_guest_cookie = True discuss_public_view_data.update({ 'shouldAddGuestAsMemberOnJoin': True, 'shouldDisplayWelcomeViewInitially': True, }) channel_sudo = channel_sudo.with_context(guest=guest) response = self._response_discuss_public_channel_template(channel_sudo=channel_sudo, discuss_public_view_data=discuss_public_view_data) if add_guest_cookie: # Discuss Guest ID: every route in this file will make use of it to authenticate # the guest through `_get_as_sudo_from_request` or `_get_as_sudo_from_request_or_raise`. expiration_date = datetime.now() + timedelta(days=365) response.set_cookie(guest._cookie_name, f"{guest.id}{guest._cookie_separator}{guest.access_token}", httponly=True, expires=expiration_date) return response
def nav_list(self, blog=None): dom = blog and [('blog_id', '=', blog.id)] or [] if not request.env.user.has_group('website.group_website_designer'): dom += [('post_date', '<=', fields.Datetime.now())] groups = request.env['blog.post']._read_group_raw( dom, ['name', 'post_date'], groupby=["post_date"], orderby="post_date desc") for group in groups: (r, label) = group['post_date'] start, end = r.split('/') group['post_date'] = label group['date_begin'] = start group['date_end'] = end locale = get_lang(request.env).code start = pytz.UTC.localize(fields.Datetime.from_string(start)) tzinfo = pytz.timezone(request.context.get('tz', 'utc') or 'utc') group['month'] = babel.dates.format_datetime(start, format='MMMM', tzinfo=tzinfo, locale=locale) group['year'] = babel.dates.format_datetime(start, format='yyyy', tzinfo=tzinfo, locale=locale) return OrderedDict((year, [m for m in months]) for year, months in itertools.groupby(groups, lambda g: g['year']))
def _name_search(self, name, args=None, operator='ilike', limit=100, name_get_uid=None): if name and operator == '=' and not args: # search on the name of the pricelist and its currency, opposite of name_get(), # Used by the magic context filter in the product search view. query_args = {'name': name, 'limit': limit, 'lang': get_lang(self.env).code} query = """SELECT p.id FROM (( SELECT pr.id, pr.name FROM product_pricelist pr JOIN res_currency cur ON (pr.currency_id = cur.id) WHERE pr.name || ' (' || cur.name || ')' = %(name)s ) UNION ( SELECT tr.res_id as id, tr.value as name FROM ir_translation tr JOIN product_pricelist pr ON ( pr.id = tr.res_id AND tr.type = 'model' AND tr.name = 'product.pricelist,name' AND tr.lang = %(lang)s ) JOIN res_currency cur ON (pr.currency_id = cur.id) WHERE tr.value || ' (' || cur.name || ')' = %(name)s ) ) p ORDER BY p.name""" if limit: query += " LIMIT %(limit)s" self._cr.execute(query, query_args) ids = [r[0] for r in self._cr.fetchall()] # regular search() to apply ACLs - may limit results below limit in some cases pricelist_ids = self._search([('id', 'in', ids)], limit=limit, access_rights_uid=name_get_uid) if pricelist_ids: return models.lazy_name_get(self.browse(pricelist_ids).with_user(name_get_uid)) return super(Pricelist, self)._name_search(name, args, operator=operator, limit=limit, name_get_uid=name_get_uid)
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 or get_lang(self.env).code lang = self.env['res.lang'].with_context(active_test=False).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
def attributes(self, record, field_name, options, values): attrs = super(DateTime, self).attributes(record, field_name, options, values) if options.get('inherit_branding'): value = record[field_name] lg = self.env['res.lang']._lang_get(self.env.user.lang) or get_lang(self.env) locale = babel.Locale.parse(lg.code) babel_format = value_format = posix_to_ldml('%s %s' % (lg.date_format, lg.time_format), locale=locale) tz = record.env.context.get('tz') or self.env.user.tz if isinstance(value, str): value = fields.Datetime.from_string(value) if value: # convert from UTC (server timezone) to user timezone value = fields.Datetime.context_timestamp(self.with_context(tz=tz), timestamp=value) value_format = pycompat.to_text(babel.dates.format_datetime(value, format=babel_format, locale=locale)) value = fields.Datetime.to_string(value) attrs['data-oe-original'] = value attrs['data-oe-original-with-format'] = value_format attrs['data-oe-original-tz'] = tz return attrs
def read_progress_bar(self, domain, group_by, progress_bar): """ Gets the data needed for all the kanban column progressbars. These are fetched alongside read_group operation. :param domain - the domain used in the kanban view to filter records :param group_by - the name of the field used to group records into kanban columns :param progress_bar - the <progressbar/> declaration attributes (field, colors, sum) :return a dictionnary mapping group_by values to dictionnaries mapping progress bar field values to the related number of records """ # Workaround to match read_group's infrastructure # TO DO in master: harmonize this function and readgroup to allow factorization group_by_modifier = group_by.partition(':')[2] or 'month' group_by = group_by.partition(':')[0] display_date_formats = { 'day': 'dd MMM yyyy', 'week': "'W'w YYYY", 'month': 'MMMM yyyy', 'quarter': 'QQQ yyyy', 'year': 'yyyy' } records_values = self.search_read(domain or [], [progress_bar['field'], group_by]) data = {} field_type = self._fields[group_by].type if field_type == 'selection': selection_labels = dict(self.fields_get()[group_by]['selection']) for record_values in records_values: group_by_value = record_values[group_by] # Again, imitating what _read_group_format_result and _read_group_prepare_data do if group_by_value and field_type in ['date', 'datetime']: locale = get_lang(self.env).code group_by_value = fields.Datetime.to_datetime(group_by_value) group_by_value = pytz.timezone('UTC').localize(group_by_value) tz_info = None if field_type == 'datetime' and self._context.get( 'tz') in pytz.all_timezones: tz_info = self._context.get('tz') group_by_value = babel.dates.format_datetime( group_by_value, format=display_date_formats[group_by_modifier], tzinfo=tz_info, locale=locale) else: group_by_value = babel.dates.format_date( group_by_value, format=display_date_formats[group_by_modifier], locale=locale) if field_type == 'selection': group_by_value = selection_labels[group_by_value] \ if group_by_value in selection_labels else False if type(group_by_value) == tuple: group_by_value = group_by_value[ 1] # FIXME should use technical value (0) if group_by_value not in data: data[group_by_value] = {} for key in progress_bar['colors']: data[group_by_value][key] = 0 field_value = record_values[progress_bar['field']] if field_value in data[group_by_value]: data[group_by_value][field_value] += 1 return data
def generate_fec(self): self.ensure_one() if not (self.env.is_admin() or self.env.user.has_group('account.group_account_user')): raise AccessDenied() # We choose to implement the flat file instead of the XML # file for 2 reasons : # 1) the XSD file impose to have the label on the account.move # but Odoo has the label on the account.move.line, so that's a # problem ! # 2) CSV files are easier to read/use for a regular accountant. # So it will be easier for the accountant to check the file before # sending it to the fiscal administration today = fields.Date.today() if self.date_from > today or self.date_to > today: raise UserError( _('You could not set the start date or the end date in the future.' )) if self.date_from >= self.date_to: raise UserError( _('The start date must be inferior to the end date.')) company = self.env.company company_legal_data = self._get_company_legal_data(company) header = [ u'JournalCode', # 0 u'JournalLib', # 1 u'EcritureNum', # 2 u'EcritureDate', # 3 u'CompteNum', # 4 u'CompteLib', # 5 u'CompAuxNum', # 6 We use partner.id u'CompAuxLib', # 7 u'PieceRef', # 8 u'PieceDate', # 9 u'EcritureLib', # 10 u'Debit', # 11 u'Credit', # 12 u'EcritureLet', # 13 u'DateLet', # 14 u'ValidDate', # 15 u'Montantdevise', # 16 u'Idevise', # 17 ] rows_to_write = [header] # INITIAL BALANCE unaffected_earnings_xml_ref = self.env.ref( 'account.data_unaffected_earnings') unaffected_earnings_line = True # used to make sure that we add the unaffected earning initial balance only once if unaffected_earnings_xml_ref: #compute the benefit/loss of last year to add in the initial balance of the current year earnings account unaffected_earnings_results = self._do_query_unaffected_earnings() unaffected_earnings_line = False sql_query = ''' SELECT 'OUV' AS JournalCode, 'Balance initiale' AS JournalLib, 'OUVERTURE/' || %s AS EcritureNum, %s AS EcritureDate, MIN(aa.code) AS CompteNum, replace(replace(MIN(aa.name), '|', '/'), '\t', '') AS CompteLib, '' AS CompAuxNum, '' AS CompAuxLib, '-' AS PieceRef, %s AS PieceDate, '/' AS EcritureLib, replace(CASE WHEN sum(aml.balance) <= 0 THEN '0,00' ELSE to_char(SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Debit, replace(CASE WHEN sum(aml.balance) >= 0 THEN '0,00' ELSE to_char(-SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Credit, '' AS EcritureLet, '' AS DateLet, %s AS ValidDate, '' AS Montantdevise, '' AS Idevise, MIN(aa.id) AS CompteID FROM account_move_line aml LEFT JOIN account_move am ON am.id=aml.move_id JOIN account_account aa ON aa.id = aml.account_id LEFT JOIN account_account_type aat ON aa.user_type_id = aat.id WHERE am.date < %s AND am.company_id = %s AND aat.include_initial_balance = 't' ''' # For official report: only use posted entries if self.export_type == "official": sql_query += ''' AND am.state = 'posted' ''' sql_query += ''' GROUP BY aml.account_id, aat.type HAVING aat.type not in ('receivable', 'payable') ''' formatted_date_from = fields.Date.to_string(self.date_from).replace( '-', '') date_from = self.date_from formatted_date_year = date_from.year currency_digits = 2 self._cr.execute( sql_query, (formatted_date_year, formatted_date_from, formatted_date_from, formatted_date_from, self.date_from, company.id)) for row in self._cr.fetchall(): listrow = list(row) account_id = listrow.pop() if not unaffected_earnings_line: account = self.env['account.account'].browse(account_id) if account.user_type_id.id == self.env.ref( 'account.data_unaffected_earnings').id: #add the benefit/loss of previous fiscal year to the first unaffected earnings account found. unaffected_earnings_line = True current_amount = float(listrow[11].replace( ',', '.')) - float(listrow[12].replace(',', '.')) unaffected_earnings_amount = float( unaffected_earnings_results[11].replace( ',', '.')) - float( unaffected_earnings_results[12].replace( ',', '.')) listrow_amount = current_amount + unaffected_earnings_amount if float_is_zero(listrow_amount, precision_digits=currency_digits): continue if listrow_amount > 0: listrow[11] = str(listrow_amount).replace('.', ',') listrow[12] = '0,00' else: listrow[11] = '0,00' listrow[12] = str(-listrow_amount).replace('.', ',') rows_to_write.append(listrow) #if the unaffected earnings account wasn't in the selection yet: add it manually if (not unaffected_earnings_line and unaffected_earnings_results and (unaffected_earnings_results[11] != '0,00' or unaffected_earnings_results[12] != '0,00')): #search an unaffected earnings account unaffected_earnings_account = self.env['account.account'].search( [('user_type_id', '=', self.env.ref('account.data_unaffected_earnings').id)], limit=1) if unaffected_earnings_account: unaffected_earnings_results[ 4] = unaffected_earnings_account.code unaffected_earnings_results[ 5] = unaffected_earnings_account.name rows_to_write.append(unaffected_earnings_results) # INITIAL BALANCE - receivable/payable sql_query = ''' SELECT 'OUV' AS JournalCode, 'Balance initiale' AS JournalLib, 'OUVERTURE/' || %s AS EcritureNum, %s AS EcritureDate, MIN(aa.code) AS CompteNum, replace(MIN(aa.name), '|', '/') AS CompteLib, CASE WHEN MIN(aat.type) IN ('receivable', 'payable') THEN CASE WHEN rp.ref IS null OR rp.ref = '' THEN rp.id::text ELSE replace(rp.ref, '|', '/') END ELSE '' END AS CompAuxNum, CASE WHEN aat.type IN ('receivable', 'payable') THEN COALESCE(replace(rp.name, '|', '/'), '') ELSE '' END AS CompAuxLib, '-' AS PieceRef, %s AS PieceDate, '/' AS EcritureLib, replace(CASE WHEN sum(aml.balance) <= 0 THEN '0,00' ELSE to_char(SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Debit, replace(CASE WHEN sum(aml.balance) >= 0 THEN '0,00' ELSE to_char(-SUM(aml.balance), '000000000000000D99') END, '.', ',') AS Credit, '' AS EcritureLet, '' AS DateLet, %s AS ValidDate, '' AS Montantdevise, '' AS Idevise, MIN(aa.id) AS CompteID FROM account_move_line aml LEFT JOIN account_move am ON am.id=aml.move_id LEFT JOIN res_partner rp ON rp.id=aml.partner_id JOIN account_account aa ON aa.id = aml.account_id LEFT JOIN account_account_type aat ON aa.user_type_id = aat.id WHERE am.date < %s AND am.company_id = %s AND aat.include_initial_balance = 't' ''' # For official report: only use posted entries if self.export_type == "official": sql_query += ''' AND am.state = 'posted' ''' sql_query += ''' GROUP BY aml.account_id, aat.type, rp.ref, rp.id HAVING aat.type in ('receivable', 'payable') ''' self._cr.execute( sql_query, (formatted_date_year, formatted_date_from, formatted_date_from, formatted_date_from, self.date_from, company.id)) for row in self._cr.fetchall(): listrow = list(row) account_id = listrow.pop() rows_to_write.append(listrow) # LINES sql_query = ''' SELECT REGEXP_REPLACE(replace(aj.code, '|', '/'), '[\\t\\r\\n]', ' ', 'g') AS JournalCode, REGEXP_REPLACE(replace(COALESCE(aj__name.value, aj.name), '|', '/'), '[\\t\\r\\n]', ' ', 'g') AS JournalLib, REGEXP_REPLACE(replace(am.name, '|', '/'), '[\\t\\r\\n]', ' ', 'g') AS EcritureNum, TO_CHAR(am.date, 'YYYYMMDD') AS EcritureDate, aa.code AS CompteNum, REGEXP_REPLACE(replace(aa.name, '|', '/'), '[\\t\\r\\n]', ' ', 'g') AS CompteLib, CASE WHEN aat.type IN ('receivable', 'payable') THEN CASE WHEN rp.ref IS null OR rp.ref = '' THEN rp.id::text ELSE replace(rp.ref, '|', '/') END ELSE '' END AS CompAuxNum, CASE WHEN aat.type IN ('receivable', 'payable') THEN COALESCE(REGEXP_REPLACE(replace(rp.name, '|', '/'), '[\\t\\r\\n]', ' ', 'g'), '') ELSE '' END AS CompAuxLib, CASE WHEN am.ref IS null OR am.ref = '' THEN '-' ELSE REGEXP_REPLACE(replace(am.ref, '|', '/'), '[\\t\\r\\n]', ' ', 'g') END AS PieceRef, TO_CHAR(am.date, 'YYYYMMDD') AS PieceDate, CASE WHEN aml.name IS NULL OR aml.name = '' THEN '/' WHEN aml.name SIMILAR TO '[\\t|\\s|\\n]*' THEN '/' ELSE REGEXP_REPLACE(replace(aml.name, '|', '/'), '[\\t\\n\\r]', ' ', 'g') END AS EcritureLib, replace(CASE WHEN aml.debit = 0 THEN '0,00' ELSE to_char(aml.debit, '000000000000000D99') END, '.', ',') AS Debit, replace(CASE WHEN aml.credit = 0 THEN '0,00' ELSE to_char(aml.credit, '000000000000000D99') END, '.', ',') AS Credit, CASE WHEN rec.name IS NULL THEN '' ELSE rec.name END AS EcritureLet, CASE WHEN aml.full_reconcile_id IS NULL THEN '' ELSE TO_CHAR(rec.create_date, 'YYYYMMDD') END AS DateLet, TO_CHAR(am.date, 'YYYYMMDD') AS ValidDate, CASE WHEN aml.amount_currency IS NULL OR aml.amount_currency = 0 THEN '' ELSE replace(to_char(aml.amount_currency, '000000000000000D99'), '.', ',') END AS Montantdevise, CASE WHEN aml.currency_id IS NULL THEN '' ELSE rc.name END AS Idevise FROM account_move_line aml LEFT JOIN account_move am ON am.id=aml.move_id LEFT JOIN res_partner rp ON rp.id=aml.partner_id JOIN account_journal aj ON aj.id = am.journal_id LEFT JOIN ir_translation aj__name ON aj__name.res_id = aj.id AND aj__name.type = 'model' AND aj__name.name = 'account.journal,name' AND aj__name.lang = %s AND aj__name.value != '' JOIN account_account aa ON aa.id = aml.account_id LEFT JOIN account_account_type aat ON aa.user_type_id = aat.id LEFT JOIN res_currency rc ON rc.id = aml.currency_id LEFT JOIN account_full_reconcile rec ON rec.id = aml.full_reconcile_id WHERE am.date >= %s AND am.date <= %s AND am.company_id = %s ''' # For official report: only use posted entries if self.export_type == "official": sql_query += ''' AND am.state = 'posted' ''' sql_query += ''' ORDER BY am.date, am.name, aml.id ''' lang = self.env.user.lang or get_lang(self.env).code self._cr.execute(sql_query, (lang, self.date_from, self.date_to, company.id)) for row in self._cr.fetchall(): rows_to_write.append(list(row)) fecvalue = self._csv_write_rows(rows_to_write) end_date = fields.Date.to_string(self.date_to).replace('-', '') suffix = '' if self.export_type == "nonofficial": suffix = '-NONOFFICIAL' self.write({ 'fec_data': base64.encodebytes(fecvalue), # Filename = <siren>FECYYYYMMDD where YYYMMDD is the closing date 'filename': '%sFEC%s%s.csv' % (company_legal_data, end_date, suffix), }) # Set fiscal year lock date to the end date (not in test) fiscalyear_lock_date = self.env.company.fiscalyear_lock_date if not self.test_file and (not fiscalyear_lock_date or fiscalyear_lock_date < self.date_to): self.env.company.write({'fiscalyear_lock_date': self.date_to}) return { 'name': 'FEC', 'type': 'ir.actions.act_url', 'url': "web/content/?model=account.fr.fec&id=" + str(self.id) + "&filename_field=filename&field=fec_data&download=true&filename=" + self.filename, 'target': 'self', }
def product_id_change(self): if not self.product_id: return if not self.product_id.product_tmpl_id: return self.tipo_de_producto = self.product_id.product_tmpl_id.categ_id.name self.cantidad_por_caja_master = self.product_id.product_tmpl_id.cantidad_por_caja_master if self.cantidad_por_caja_master != 0: self.volumen_total_de_linea_producto = self.product_uom_qty * self.volumen_caja_master / self.cantidad_por_caja_master valid_values = self.product_id.product_tmpl_id.valid_product_template_attribute_line_ids.product_template_value_ids # remove the is_custom values that don't belong to this template for pacv in self.product_custom_attribute_value_ids: if pacv.custom_product_template_attribute_value_id not in valid_values: self.product_custom_attribute_value_ids -= pacv # remove the no_variant attributes that don't belong to this template for ptav in self.product_no_variant_attribute_value_ids: if ptav._origin not in valid_values: self.product_no_variant_attribute_value_ids -= ptav vals = {} for price_list_item in self.order_id.pricelist_id.item_ids: if price_list_item.base == 'purchase': if price_list_item.product_tmpl_id.id == self.product_id.product_tmpl_id.id: vals['price_list_item'] = price_list_item.id vals['price_unit'] = price_list_item.total_margin if not self.product_uom or (self.product_id.uom_id.id != self.product_uom.id): vals['product_uom'] = self.product_id.uom_id vals['product_uom_qty'] = self.product_uom_qty or 1.0 product = self.product_id.with_context( lang=get_lang(self.env, self.order_id.partner_id.lang).code, partner=self.order_id.partner_id, quantity=vals.get('product_uom_qty') or self.product_uom_qty, date=self.order_id.date_order, pricelist=self.order_id.pricelist_id.id, uom=self.product_uom.id) vals.update( name=self.get_sale_order_line_multiline_description_sale(product)) self._compute_tax_id() # if self.order_id.pricelist_id and self.order_id.partner_id: # vals['price_unit'] = self.env['account.tax']._fix_tax_included_price_company(self._get_display_price(product), product.taxes_id, self.tax_id, self.company_id) for price_list_item in self.order_id.pricelist_id.item_ids: if price_list_item.base == 'purchase': if price_list_item.product_tmpl_id.id == self.product_id.product_tmpl_id.id: vals['price_list_item'] = price_list_item.id vals['price_unit'] = price_list_item.total_margin self.update(vals) title = False message = False result = {} warning = {} if product.sale_line_warn != 'no-message': title = _("Warning for %s") % product.name message = product.sale_line_warn_msg warning['title'] = title warning['message'] = message result = {'warning': warning} if product.sale_line_warn == 'block': self.product_id = False return result
def action_invoice_sent(self): """ Open a window to compose an email, with the edi invoice template message loaded by default """ self.ensure_one() template = self.env.ref( 'l10n_co_e_invoicing.email_template_for_einvoice') # template = self.env.ref('account.email_template_edi_invoice', raise_if_not_found=False) xml_attachment_file = False if self.dian_document_lines[ 0].ar_xml_file and self.dian_document_lines[0].xml_file: xml_without_signature = global_functions.get_template_xml( self.dian_document_lines._get_attachment_values(), 'attachment') xml_attachment_file = self.env['ir.attachment'].create({ 'name': self.name + '-attachment.xml', 'type': 'binary', 'datas': b64encode(xml_without_signature.encode()).decode( "utf-8", "ignore") }) xml_attachment = self.env['ir.attachment'].create({ 'name': self.dian_document_lines.xml_filename, 'type': 'binary', 'datas': self.dian_document_lines.xml_file }) pdf_attachment = self.env['ir.attachment'].create({ 'name': self.name + '.pdf', 'type': 'binary', 'datas': self._get_pdf_file() }) attach_ids = [(xml_attachment.id), (pdf_attachment.id)] if xml_attachment_file: attach_ids.append((xml_attachment_file.id)) template.attachment_ids = [(6, 0, attach_ids)] lang = get_lang(self.env) if template and template.lang: lang = template._render_template(template.lang, 'account.move', self.id) else: lang = lang.code compose_form = self.env.ref('account.account_invoice_send_wizard_form', raise_if_not_found=False) ctx = dict(default_model='account.move', default_res_id=self.id, default_use_template=bool(template), default_template_id=template and template.id or False, default_composition_mode='comment', mark_invoice_as_sent=True, custom_layout="mail.mail_notification_paynow", model_description=self.with_context(lang=lang).type_name, force_email=True) return { 'name': _('Send Invoice'), 'type': 'ir.actions.act_window', 'view_type': 'form', 'view_mode': 'form', 'res_model': 'account.invoice.send', 'views': [(compose_form.id, 'form')], 'view_id': compose_form.id, 'target': 'new', 'context': ctx, }
def _get_appointment_slots(self, timezone, employee=None): """ Fetch available slots to book an appointment :param timezone: timezone string e.g.: 'Europe/Brussels' or 'Etc/GMT+1' :param employee: if set will only check available slots for this employee :returns: list of dicts (1 per month) containing available slots per day per week. complex structure used to simplify rendering of template """ self.ensure_one() appt_tz = pytz.timezone(self.appointment_tz) requested_tz = pytz.timezone(timezone) first_day = requested_tz.fromutc(datetime.utcnow() + relativedelta( hours=self.min_schedule_hours)) last_day = requested_tz.fromutc(datetime.utcnow() + relativedelta( days=self.max_schedule_days)) # Compute available slots (ordered) slots = self._slots_generate(first_day.astimezone(appt_tz), last_day.astimezone(appt_tz), timezone) if not employee or employee in self.employee_ids: self._slots_available(slots, first_day.astimezone(pytz.UTC), last_day.astimezone(pytz.UTC), employee) # Compute calendar rendering and inject available slots today = requested_tz.fromutc(datetime.utcnow()) start = today month_dates_calendar = cal.Calendar(0).monthdatescalendar months = [] while (start.year, start.month) <= (last_day.year, last_day.month): dates = month_dates_calendar(start.year, start.month) for week_index, week in enumerate(dates): for day_index, day in enumerate(week): mute_cls = weekend_cls = today_cls = None today_slots = [] if day.weekday() in (cal.SUNDAY, cal.SATURDAY): weekend_cls = 'o_weekend' if day == today.date() and day.month == today.month: today_cls = 'o_today' if day.month != start.month: mute_cls = 'text-muted o_mute_day' else: # slots are ordered, so check all unprocessed slots from until > day while slots and (slots[0][timezone][0].date() <= day): if (slots[0][timezone][0].date() == day) and ('employee_id' in slots[0]): today_slots.append({ 'employee_id': slots[0]['employee_id'].id, 'datetime': slots[0][timezone][0].strftime( '%Y-%m-%d %H:%M:%S'), 'hours': slots[0][timezone][0].strftime('%H:%M') }) slots.pop(0) dates[week_index][day_index] = { 'day': day, 'slots': today_slots, 'mute_cls': mute_cls, 'weekend_cls': weekend_cls, 'today_cls': today_cls } months.append({ 'month': format_datetime(start, 'MMMM Y', locale=get_lang(self.env).code), 'weeks': dates }) start = start + relativedelta(months=1) return months
def _get_json_activity_data(self): for journal in self: activities = [] # search activity on move on the journal sql_query = ''' SELECT act.id, act.res_id, act.res_model, act.summary, act_type.name as act_type_name, act_type.category as activity_category, act.date_deadline, m.date, CASE WHEN act.date_deadline < CURRENT_DATE THEN 'late' ELSE 'future' END as status FROM account_move m LEFT JOIN mail_activity act ON act.res_id = m.id LEFT JOIN mail_activity_type act_type ON act.activity_type_id = act_type.id WHERE act.res_model = 'account.move' AND m.journal_id = %s ''' self.env.cr.execute(sql_query, (journal.id,)) for activity in self.env.cr.dictfetchall(): act = { 'id': activity.get('id'), 'res_id': activity.get('res_id'), 'res_model': activity.get('res_model'), 'status': activity.get('status'), 'name': (activity.get('summary') or activity.get('act_type_name')), 'activity_category': activity.get('activity_category'), 'date': odoo_format_date(self.env, activity.get('date_deadline')) } if activity.get('activity_category') == 'tax_report' and activity.get('res_model') == 'account.move': if self.env['account.move'].browse(activity.get('res_id')).company_id.account_tax_periodicity == 'monthly': act['name'] += ' (' + format_date(activity.get('date'), 'MMM', locale=get_lang(self.env).code) + ')' else: act['name'] += ' (' + format_date(activity.get('date'), 'QQQ', locale=get_lang(self.env).code) + ')' activities.append(act) journal.json_activity_data = json.dumps({'activities': activities})
def get_formated_blog_date(self, blog): post_date = fields.Datetime.from_string(blog.post_date).date() month = babel.dates.get_month_names( 'abbreviated', locale=get_lang(blog.env).code)[post_date.month] return ('%s %s') % (month, post_date.strftime("%e"))
def _compute_sale_graph(self, date_from, date_to, sales_domain, previous=False): days_between = (date_to - date_from).days date_list = [(date_from + timedelta(days=x)) for x in range(0, days_between + 1)] daily_sales = request.env['sale.report'].read_group( domain=sales_domain, fields=['date', 'price_subtotal'], groupby='date:day') daily_sales_dict = {p['date:day']: p['price_subtotal'] for p in daily_sales} sales_graph = [{ '0': fields.Date.to_string(d) if not previous else fields.Date.to_string(d + timedelta(days=days_between)), # Respect read_group format in models.py '1': daily_sales_dict.get(babel.dates.format_date(d, format='dd MMM yyyy', locale=get_lang(request.env).code), 0) } for d in date_list] return sales_graph
def _to_short_month_name(date): month_index = fields.Date.from_string(date).month return babel.dates.get_month_names('abbreviated', locale=get_lang(self.env).code)[month_index]
def get_bar_graph_datas(self): data = [] today = fields.Datetime.now(self) data.append({'label': _('Due'), 'value': 0.0, 'type': 'past'}) day_of_week = int( format_datetime(today, 'e', locale=get_lang(self.env).code)) first_day_of_week = today + timedelta(days=-day_of_week + 1) for i in range(-1, 4): if i == 0: label = _('This Week') elif i == 3: label = _('Not Due') else: start_week = first_day_of_week + timedelta(days=i * 7) end_week = start_week + timedelta(days=6) if start_week.month == end_week.month: label = str(start_week.day) + '-' + str( end_week.day) + ' ' + format_date( end_week, 'MMM', locale=get_lang(self.env).code) else: label = format_date( start_week, 'd MMM', locale=get_lang(self.env).code) + '-' + format_date( end_week, 'd MMM', locale=get_lang(self.env).code) data.append({ 'label': label, 'value': 0.0, 'type': 'past' if i < 0 else 'future' }) # Build SQL query to find amount aggregated by week (select_sql_clause, query_args) = self._get_bar_graph_select_query() query = '' start_date = (first_day_of_week + timedelta(days=-7)) for i in range(0, 6): if i == 0: query += "(" + select_sql_clause + " and invoice_date_due < '" + start_date.strftime( DF) + "')" elif i == 5: query += " UNION ALL (" + select_sql_clause + " and invoice_date_due >= '" + start_date.strftime( DF) + "')" else: next_date = start_date + timedelta(days=7) query += " UNION ALL (" + select_sql_clause + " and invoice_date_due >= '" + start_date.strftime( DF) + "' and invoice_date_due < '" + next_date.strftime( DF) + "')" start_date = next_date self.env.cr.execute(query, query_args) query_results = self.env.cr.dictfetchall() is_sample_data = True for index in range(0, len(query_results)): if query_results[index].get('aggr_date') != None: is_sample_data = False data[index]['value'] = query_results[index].get('total') [graph_title, graph_key] = self._graph_title_and_key() if is_sample_data: for index in range(0, len(query_results)): data[index]['type'] = 'o_sample_data' # we use unrealistic values for the sample data data[index]['value'] = random.randint(0, 20) graph_key = _('Sample data') return [{ 'values': data, 'title': graph_title, 'key': graph_key, 'is_sample_data': is_sample_data }]
def calendar_appointment_form(self, appointment_type, date_time, duration, types=False, **kwargs): #timezone = self._context.get('tz') or self.env.user.partner_id.tz or 'UTC' #timezone = pytz.timezone(self.event_tz) if self.event_tz else pytz.timezone(self._context.get('tz') or 'UTC') request.session['timezone'] = appointment_type.appointment_tz or 'UTC' partner_data = {} # if request.env.user.partner_id != request.env.ref('base.public_partner'): # partner_data = request.env.user.partner_id.read(fields=['name', 'mobile', 'email'])[0] request.session['timezone'] = appointment_type.appointment_tz #day_name = format_datetime(datetime.strptime(date_time, dtf), 'EEE', locale=get_lang(request.env).code) day_name = format_datetime(datetime.strptime(date_time, "%Y-%m-%d %H:%M"), 'EEE', locale=get_lang(request.env).code) #date_formated = format_datetime(datetime.strptime(date_time, dtf), locale=get_lang(request.env).code) date_formated = format_datetime(datetime.strptime(date_time, "%Y-%m-%d %H:%M"), locale=get_lang(request.env).code) city_code = appointment_type.judged_id.city_id.id employee_id = appointment_type.judged_id.hr_employee_id.id employee_obj = request.env['hr.employee'].sudo().browse(int(employee_id)) timezone = request.session['timezone'] tz_session = pytz.timezone(timezone) date_start = tz_session.localize(fields.Datetime.from_string(date_time)).astimezone(pytz.utc) date_end = date_start + relativedelta(hours=float(duration)) # check availability calendar.event with partner of appointment_type if appointment_type and appointment_type.judged_id: if not appointment_type.judged_id.calendar_verify_availability(date_start,date_end): return request.render("website_calendar.index", { 'appointment_type': appointment_type, 'suggested_appointment_types': request.env['calendar.appointment.type'].sudo().search([('id','=',appointment_type.id)]), 'message': 'already_scheduling', 'date_start': date_start, 'date_end': date_end, 'types': types, }) if types[0] == 'A': suggested_class = request.env['calendar.class'].sudo().search([('type','=','audience')]) else: suggested_class = request.env['calendar.class'].sudo().search([('type','=','other')]) suggested_help1 = request.env['calendar.help'].sudo().search([('type','=','support')]) suggested_help2 = request.env['calendar.help'].sudo().search([('type','=','type_p')]) suggested_help3 = request.env['calendar.help'].sudo().search([('type','=','type_c')]) suggested_rooms = request.env['res.judged.room'].sudo().search_city(city_code) suggested_partners = request.env['res.partner'].sudo().search([]) #suggested_companies = request.env['res.partner'].sudo().search_company_type() suggested_reception = request.env['calendar.reception'].sudo().search([]) #return request.render("website_calendar.appointment_form", { return request.render("calendar_csj.appointment_form_csj", { 'partner_data': partner_data, 'appointment_type': appointment_type, 'suggested_class': suggested_class, 'suggested_partners': suggested_partners, #'suggested_companies': suggested_companies, 'suggested_reception': suggested_reception, 'suggested_rooms': suggested_rooms, 'suggested_help1': suggested_help1, 'suggested_help2': suggested_help2, 'suggested_help3': suggested_help3, 'types': types, 'duration': duration, 'datetime': date_time, 'datetime_locale': day_name + ' ' + date_time, 'datetime_str': date_time, 'employee_id': employee_id, 'duration': duration, 'countries': request.env['res.country'].search([]), })
def default_get(self, fields): res = super(BaseGengoTranslations, self).default_get(fields) res['authorized_credentials'], gengo = self.gengo_authentication() if 'lang_id' in fields: res['lang_id'] = get_lang(self.env).id return res
def get_formated_date(self, event): start_date = fields.Datetime.from_string(event.date_begin).date() end_date = fields.Datetime.from_string(event.date_end).date() month = babel.dates.get_month_names('abbreviated', locale=get_lang(event.env).code)[start_date.month] return ('%s %s%s') % (month, start_date.strftime("%e"), (end_date != start_date and ("-" + end_date.strftime("%e")) or ""))
def _get_date_formats(self): """ get current date and time format, according to the context lang :return: a tuple with (format date, format time) """ lang = get_lang(self.env) return (lang.date_format, lang.time_format)
def get_line_graph_datas(self): """Computes the data used to display the graph for bank and cash journals in the accounting dashboard""" currency = self.currency_id or self.company_id.currency_id def build_graph_data(date, amount): #display date in locale format name = format_date(date, 'd LLLL Y', locale=locale) short_name = format_date(date, 'd MMM', locale=locale) return {'x': short_name, 'y': amount, 'name': name} self.ensure_one() BankStatement = self.env['account.bank.statement'] data = [] today = datetime.today() last_month = today + timedelta(days=-30) locale = get_lang(self.env).code #starting point of the graph is the last statement last_stmt = self._get_last_bank_statement(domain=[('move_id.state', '=', 'posted')]) last_balance = last_stmt and last_stmt.balance_end_real or 0 data.append(build_graph_data(today, last_balance)) #then we subtract the total amount of bank statement lines per day to get the previous points #(graph is drawn backward) date = today amount = last_balance query = ''' SELECT move.date, sum(st_line.amount) as amount FROM account_bank_statement_line st_line JOIN account_move move ON move.id = st_line.move_id WHERE move.journal_id = %s AND move.date > %s AND move.date <= %s GROUP BY move.date ORDER BY move.date desc ''' self.env.cr.execute(query, (self.id, last_month, today)) query_result = self.env.cr.dictfetchall() for val in query_result: date = val['date'] if date != today.strftime( DF): # make sure the last point in the graph is today data[:0] = [build_graph_data(date, amount)] amount = currency.round(amount - val['amount']) # make sure the graph starts 1 month ago if date.strftime(DF) != last_month.strftime(DF): data[:0] = [build_graph_data(last_month, amount)] [graph_title, graph_key] = self._graph_title_and_key() color = '#875A7B' if 'e' in version else '#7c7bad' is_sample_data = not last_stmt and len(query_result) == 0 if is_sample_data: data = [] for i in range(30, 0, -5): current_date = today + timedelta(days=-i) data.append( build_graph_data(current_date, random.randint(-5, 15))) return [{ 'values': data, 'title': graph_title, 'key': graph_key, 'area': True, 'color': color, 'is_sample_data': is_sample_data }]
def action_invoice_sent(self): """ Open a window to compose an email, with the edi invoice template message loaded by default """ self.ensure_one() template = self.env.ref( 'l10n_co_e_invoicing_comfiar.email_template_for_einvoice') # template = self.env.ref('account.email_template_edi_invoice', raise_if_not_found=False) dian_document = self.dian_document_lines.filtered( lambda x: x.state == 'done') if len(dian_document) > 1: raise ValidationError( 'Hay mas de un documento DIAN en estado "Hecho", validar que solo exista uno' ) elif len(dian_document) == 1: xml_attachment_file = self.env['ir.attachment'].create( { 'name': dian_document.xml_filename, 'type': 'binary', 'datas': dian_document.xml_file } ) #b64encode(xml_without_signature.encode()).decode("utf-8", "ignore")}) pdf_attachment_file = self.env['ir.attachment'].create({ 'name': dian_document.pdf_filename, 'type': 'binary', 'datas': dian_document.pdf_file }) # pdf_attachment = self.env['ir.attachment'].create({ # 'name': self.name + '.pdf', # 'type': 'binary', # 'datas': self._get_pdf_file()}) template.attachment_ids = [(6, 0, [(pdf_attachment_file.id), (xml_attachment_file.id)])] lang = get_lang(self.env) if template and template.lang: lang = template._render_template(template.lang, 'account.move', self.id) else: lang = lang.code compose_form = self.env.ref('account.account_invoice_send_wizard_form', raise_if_not_found=False) ctx = dict(default_model='account.move', default_res_id=self.id, default_use_template=bool(template), default_template_id=template and template.id or False, default_composition_mode='comment', mark_invoice_as_sent=True, custom_layout="mail.mail_notification_paynow", model_description=self.with_context(lang=lang).type_name, force_email=True) return { 'name': _('Send Invoice'), 'type': 'ir.actions.act_window', 'view_type': 'form', 'view_mode': 'form', 'res_model': 'account.invoice.send', 'views': [(compose_form.id, 'form')], 'view_id': compose_form.id, 'target': 'new', 'context': ctx, }