def search_paid_order_ids(self, config_id, domain, limit, offset): """Search for 'paid' orders that satisfy the given domain, limit and offset.""" default_domain = ['&', ('config_id', '=', config_id), '!', '|', ('state', '=', 'draft'), ('state', '=', 'cancelled')] real_domain = AND([domain, default_domain]) ids = self.search(AND([domain, default_domain]), limit=limit, offset=offset).ids totalCount = self.search_count(real_domain) return {'ids': ids, 'totalCount': totalCount}
def _bom_subcontract_find(self, product_tmpl=None, product=None, picking_type=None, company_id=False, bom_type='subcontract', subcontractor=False): domain = self._bom_find_domain(product_tmpl=product_tmpl, product=product, picking_type=picking_type, company_id=company_id, bom_type=bom_type) if subcontractor: domain = AND([domain, [('subcontractor_ids', 'parent_of', subcontractor.ids)]]) return self.search(domain, order='sequence, product_id', limit=1) else: return self.env['mrp.bom']
def pos_web(self, config_id=False, **k): """Open a pos session for the given config. The right pos session will be selected to open, if non is open yet a new session will be created. /pos/ui and /pos/web both can be used to acces the POS. On the SaaS, /pos/ui uses HTTPS while /pos/web uses HTTP. :param debug: The debug mode to load the session in. :type debug: str. :param config_id: id of the config that has to be loaded. :type config_id: str. :returns: object -- The rendered pos session. """ domain = [('state', 'in', ['opening_control', 'opened']), ('user_id', '=', request.session.uid), ('rescue', '=', False)] if config_id: domain = AND([domain, [('config_id', '=', int(config_id))]]) pos_session = request.env['pos.session'].sudo().search(domain, limit=1) # The same POS session can be opened by a different user => search without restricting to # current user. Note: the config must be explicitly given to avoid fallbacking on a random # session. if not pos_session and config_id: domain = [ ('state', 'in', ['opening_control', 'opened']), ('rescue', '=', False), ('config_id', '=', int(config_id)), ] pos_session = request.env['pos.session'].sudo().search(domain, limit=1) if not pos_session: return werkzeug.utils.redirect( '/web#action=point_of_sale.action_client_pos_menu') # The POS only work in one company, so we enforce the one of the session in the context company = pos_session.company_id session_info = request.env['ir.http'].session_info() session_info['user_context']['allowed_company_ids'] = company.ids session_info['user_companies'] = { 'current_company': (company.id, company.name), 'allowed_companies': [(company.id, company.name)] } context = { 'session_info': session_info, 'login_number': pos_session.login(), } response = request.render('point_of_sale.index', context) response.headers['Cache-Control'] = 'no-store' return response
def _search_panel_domain_image(self, field_name, domain, set_count=False, limit=False): """ Return the values in the image of the provided domain by field_name. :param domain: domain whose image is returned :param field_name: the name of a field (type many2one or selection) :param set_count: whether to set the key '__count' in image values. Default is False. :param limit: integer, maximal number of values to fetch. Default is False. :return: a dict of the form { id: { 'id': id, 'display_name': display_name, ('__count': c,) }, ... } """ field = self._fields[field_name] if field.type == 'many2one': def group_id_name(value): return value else: # field type is selection: see doc above desc = self.fields_get([field_name])[field_name] field_name_selection = dict(desc['selection']) def group_id_name(value): return value, field_name_selection[value] domain = AND([ domain, [(field_name, '!=', False)], ]) groups = self.read_group(domain, [field_name], [field_name], limit=limit) domain_image = {} for group in groups: id, display_name = group_id_name(group[field_name]) values = { 'id': id, 'display_name': display_name, } if set_count: values['__count'] = group[field_name + '_count'] domain_image[id] = values return domain_image
def _search_panel_field_image(self, field_name, **kwargs): """ Return the values in the image of the provided domain by field_name. :param model_domain: domain whose image is returned :param extra_domain: extra domain to use when counting records associated with field values :param field_name: the name of a field (type many2one or selection) :param enable_counters: whether to set the key '__count' in image values :param only_counters: whether to retrieve information on the model_domain image or only counts based on model_domain and extra_domain. In the later case, the counts are set whatever is enable_counters. :param limit: integer, maximal number of values to fetch :param set_limit: boolean, whether to use the provided limit (if any) :return: a dict of the form { id: { 'id': id, 'display_name': display_name, ('__count': c,) }, ... } """ enable_counters = kwargs.get('enable_counters') only_counters = kwargs.get('only_counters') extra_domain = kwargs.get('extra_domain', []) no_extra = is_true_domain(extra_domain) model_domain = kwargs.get('model_domain', []) count_domain = AND([model_domain, extra_domain]) limit = kwargs.get('limit') set_limit = kwargs.get('set_limit') if only_counters: return self._search_panel_domain_image(field_name, count_domain, True) model_domain_image = self._search_panel_domain_image( field_name, model_domain, enable_counters and no_extra, set_limit and limit, ) if enable_counters and not no_extra: count_domain_image = self._search_panel_domain_image( field_name, count_domain, True) for id, values in model_domain_image.items(): element = count_domain_image.get(id) values['__count'] = element['__count'] if element else 0 return model_domain_image
def transfer_leaves_to(self, other_calendar, resources=None, from_date=None): """ Transfer some resource.calendar.leaves from 'self' to another calendar 'other_calendar'. Transfered leaves linked to `resources` (or all if `resources` is None) and starting after 'from_date' (or today if None). """ from_date = from_date or fields.Datetime.now().replace( hour=0, minute=0, second=0, microsecond=0) domain = [ ('calendar_id', 'in', self.ids), ('date_from', '>=', from_date), ] domain = AND([domain, [('resource_id', 'in', resources.ids)]]) if resources else domain self.env['resource.calendar.leaves'].search(domain).write({ 'calendar_id': other_calendar.id, })
def search_panel_select_multi_range(self, field_name, **kwargs): """ Return possible values of the field field_name (case select="multi"), possibly with counters and groups. :param field_name: the name of a filter field; possible types are many2one, many2many, selection. :param category_domain: domain generated by categories. Default is []. :param comodel_domain: domain of field values (if relational) (this parameter is used in _search_panel_range). Default is []. :param enable_counters: whether to count records by value. Default is False. :param expand: whether to return the full range of field values in comodel_domain or only the field image values. Default is False. :param filter_domain: domain generated by filters. Default is []. :param group_by: extra field to read on comodel, to group comodel records :param group_domain: dict, one domain for each activated group for the group_by (if any). Those domains are used to fech accurate counters for values in each group. Default is [] (many2one case) or None. :param limit: integer, maximal number of values to fetch. Default is None. :param search_domain: base domain of search. Default is []. :return: { 'values': a list of possible values, each being a dict with keys 'id' (value), 'name' (value label), '__count' (how many records with that value), 'group_id' (value of group), set if a group_by has been provided, 'group_name' (label of group), set if a group_by has been provided } or an object with an error message when limit is defined and reached. """ field = self._fields[field_name] supported_types = ['many2one', 'many2many', 'selection'] if field.type not in supported_types: raise UserError(_('Only types %(supported_types)s are supported for filter (found type %(field_type)s)', supported_types=supported_types, field_type=field.type)) model_domain = kwargs.get('search_domain', []) extra_domain = AND([ kwargs.get('category_domain', []), kwargs.get('filter_domain', []), ]) if field.type == 'selection': return { 'values': self._search_panel_selection_range(field_name, model_domain=model_domain, extra_domain=extra_domain, **kwargs ) } Comodel = self.env.get(field.comodel_name).with_context(hierarchical_naming=False) field_names = ['display_name'] group_by = kwargs.get('group_by') limit = kwargs.get('limit') if group_by: group_by_field = Comodel._fields[group_by] field_names.append(group_by) if group_by_field.type == 'many2one': def group_id_name(value): return value or (False, _("Not Set")) elif group_by_field.type == 'selection': desc = Comodel.fields_get([group_by])[group_by] group_by_selection = dict(desc['selection']) group_by_selection[False] = _("Not Set") def group_id_name(value): return value, group_by_selection[value] else: def group_id_name(value): return (value, value) if value else (False, _("Not Set")) comodel_domain = kwargs.get('comodel_domain', []) enable_counters = kwargs.get('enable_counters') expand = kwargs.get('expand') if field.type == 'many2many': comodel_records = Comodel.search_read(comodel_domain, field_names, limit=limit) if expand and limit and len(comodel_records) == limit: return {'error_msg': str(SEARCH_PANEL_ERROR_MESSAGE)} group_domain = kwargs.get('group_domain') field_range = [] for record in comodel_records: record_id = record['id'] values= { 'id': record_id, 'display_name': record['display_name'], } if group_by: group_id, group_name = group_id_name(record[group_by]) values['group_id'] = group_id values['group_name'] = group_name if enable_counters or not expand: search_domain = AND([ model_domain, [(field_name, 'in', record_id)], ]) local_extra_domain = extra_domain if group_by and group_domain: local_extra_domain = AND([ local_extra_domain, group_domain.get(json.dumps(group_id), []), ]) search_count_domain = AND([ search_domain, local_extra_domain ]) if enable_counters: count = self.search_count(search_count_domain) if not expand: if enable_counters and is_true_domain(local_extra_domain): inImage = count else: inImage = self.search(search_domain, limit=1) if expand or inImage: if enable_counters: values['__count'] = count field_range.append(values) if not expand and limit and len(field_range) == limit: return {'error_msg': str(SEARCH_PANEL_ERROR_MESSAGE)} return { 'values': field_range, } if field.type == 'many2one': if enable_counters or not expand: extra_domain = AND([ extra_domain, kwargs.get('group_domain', []), ]) domain_image = self._search_panel_field_image(field_name, model_domain=model_domain, extra_domain=extra_domain, only_counters=expand, set_limit=limit and not (expand or group_by or comodel_domain), **kwargs ) if not (expand or group_by or comodel_domain): values = list(domain_image.values()) if limit and len(values) == limit: return {'error_msg': str(SEARCH_PANEL_ERROR_MESSAGE)} return {'values': values, } if not expand: image_element_ids = list(domain_image.keys()) comodel_domain = AND([ comodel_domain, [('id', 'in', image_element_ids)], ]) comodel_records = Comodel.search_read(comodel_domain, field_names, limit=limit) if limit and len(comodel_records) == limit: return {'error_msg': str(SEARCH_PANEL_ERROR_MESSAGE)} field_range = [] for record in comodel_records: record_id = record['id'] values= { 'id': record_id, 'display_name': record['display_name'], } if group_by: group_id, group_name = group_id_name(record[group_by]) values['group_id'] = group_id values['group_name'] = group_name if enable_counters: image_element = domain_image.get(record_id) values['__count'] = image_element['__count'] if image_element else 0 field_range.append(values) return { 'values': field_range, }
def search_panel_select_range(self, field_name, **kwargs): """ Return possible values of the field field_name (case select="one"), possibly with counters, and the parent field (if any and required) used to hierarchize them. :param field_name: the name of a field; of type many2one or selection. :param category_domain: domain generated by categories. Default is []. :param comodel_domain: domain of field values (if relational). Default is []. :param enable_counters: whether to count records by value. Default is False. :param expand: whether to return the full range of field values in comodel_domain or only the field image values (possibly filtered and/or completed with parents if hierarchize is set). Default is False. :param filter_domain: domain generated by filters. Default is []. :param hierarchize: determines if the categories must be displayed hierarchically (if possible). If set to true and _parent_name is set on the comodel field, the information necessary for the hierarchization will be returned. Default is True. :param limit: integer, maximal number of values to fetch. Default is None. :param search_domain: base domain of search. Default is []. with parents if hierarchize is set) :return: { 'parent_field': parent field on the comodel of field, or False 'values': array of dictionaries containing some info on the records available on the comodel of the field 'field_name'. The display name, the __count (how many records with that value) and possibly parent_field are fetched. } or an object with an error message when limit is defined and is reached. """ field = self._fields[field_name] supported_types = ['many2one', 'selection'] if field.type not in supported_types: types = dict(self.env["ir.model.fields"]._fields["ttype"]._description_selection(self.env)) raise UserError(_( 'Only types %(supported_types)s are supported for category (found type %(field_type)s)', supported_types=", ".join(types[t] for t in supported_types), field_type=types[field.type], )) model_domain = kwargs.get('search_domain', []) extra_domain = AND([ kwargs.get('category_domain', []), kwargs.get('filter_domain', []), ]) if field.type == 'selection': return { 'parent_field': False, 'values': self._search_panel_selection_range(field_name, model_domain=model_domain, extra_domain=extra_domain, **kwargs ), } Comodel = self.env[field.comodel_name].with_context(hierarchical_naming=False) field_names = ['display_name'] hierarchize = kwargs.get('hierarchize', True) parent_name = False if hierarchize and Comodel._parent_name in Comodel._fields: parent_name = Comodel._parent_name field_names.append(parent_name) def get_parent_id(record): value = record[parent_name] return value and value[0] else: hierarchize = False comodel_domain = kwargs.get('comodel_domain', []) enable_counters = kwargs.get('enable_counters') expand = kwargs.get('expand') limit = kwargs.get('limit') if enable_counters or not expand: domain_image = self._search_panel_field_image(field_name, model_domain=model_domain, extra_domain=extra_domain, only_counters=expand, set_limit= limit and not (expand or hierarchize or comodel_domain), **kwargs ) if not (expand or hierarchize or comodel_domain): values = list(domain_image.values()) if limit and len(values) == limit: return {'error_msg': str(SEARCH_PANEL_ERROR_MESSAGE)} return { 'parent_field': parent_name, 'values': values, } if not expand: image_element_ids = list(domain_image.keys()) if hierarchize: condition = [('id', 'parent_of', image_element_ids)] else: condition = [('id', 'in', image_element_ids)] comodel_domain = AND([comodel_domain, condition]) comodel_records = Comodel.search_read(comodel_domain, field_names, limit=limit) if hierarchize: ids = [rec['id'] for rec in comodel_records] if expand else image_element_ids comodel_records = self._search_panel_sanitized_parent_hierarchy(comodel_records, parent_name, ids) if limit and len(comodel_records) == limit: return {'error_msg': str(SEARCH_PANEL_ERROR_MESSAGE)} field_range = {} for record in comodel_records: record_id = record['id'] values = { 'id': record_id, 'display_name': record['display_name'], } if hierarchize: values[parent_name] = get_parent_id(record) if enable_counters: image_element = domain_image.get(record_id) values['__count'] = image_element['__count'] if image_element else 0 field_range[record_id] = values if hierarchize and enable_counters: self._search_panel_global_counters(field_range, parent_name) return { 'parent_field': parent_name, 'values': list(field_range.values()), }
def get_sale_details(self, date_start=False, date_stop=False, config_ids=False, session_ids=False): """ Serialise the orders of the requested time period, configs and sessions. :param date_start: The dateTime to start, default today 00:00:00. :type date_start: str. :param date_stop: The dateTime to stop, default date_start + 23:59:59. :type date_stop: str. :param config_ids: Pos Config id's to include. :type config_ids: list of numbers. :param session_ids: Pos Config id's to include. :type session_ids: list of numbers. :returns: dict -- Serialised sales. """ domain = [('state', 'in', ['paid','invoiced','done'])] if (session_ids): domain = AND([domain, [('session_id', 'in', session_ids)]]) else: if date_start: date_start = fields.Datetime.from_string(date_start) else: # start by default today 00:00:00 user_tz = pytz.timezone(self.env.context.get('tz') or self.env.user.tz or 'UTC') today = user_tz.localize(fields.Datetime.from_string(fields.Date.context_today(self))) date_start = today.astimezone(pytz.timezone('UTC')) if date_stop: date_stop = fields.Datetime.from_string(date_stop) # avoid a date_stop smaller than date_start if (date_stop < date_start): date_stop = date_start + timedelta(days=1, seconds=-1) else: # stop by default today 23:59:59 date_stop = date_start + timedelta(days=1, seconds=-1) domain = AND([domain, [('date_order', '>=', fields.Datetime.to_string(date_start)), ('date_order', '<=', fields.Datetime.to_string(date_stop))] ]) if config_ids: domain = AND([domain, [('config_id', 'in', config_ids)]]) orders = self.env['pos.order'].search(domain) user_currency = self.env.company.currency_id total = 0.0 products_sold = {} taxes = {} for order in orders: if user_currency != order.pricelist_id.currency_id: total += order.pricelist_id.currency_id._convert( order.amount_total, user_currency, order.company_id, order.date_order or fields.Date.today()) else: total += order.amount_total currency = order.session_id.currency_id for line in order.lines: key = (line.product_id, line.price_unit, line.discount) products_sold.setdefault(key, 0.0) products_sold[key] += line.qty if line.tax_ids_after_fiscal_position: line_taxes = line.tax_ids_after_fiscal_position.sudo().compute_all(line.price_unit * (1-(line.discount or 0.0)/100.0), currency, line.qty, product=line.product_id, partner=line.order_id.partner_id or False) for tax in line_taxes['taxes']: taxes.setdefault(tax['id'], {'name': tax['name'], 'tax_amount':0.0, 'base_amount':0.0}) taxes[tax['id']]['tax_amount'] += tax['amount'] taxes[tax['id']]['base_amount'] += tax['base'] else: taxes.setdefault(0, {'name': _('No Taxes'), 'tax_amount':0.0, 'base_amount':0.0}) taxes[0]['base_amount'] += line.price_subtotal_incl payment_ids = self.env["pos.payment"].search([('pos_order_id', 'in', orders.ids)]).ids if payment_ids: self.env.cr.execute(""" SELECT method.name, sum(amount) total FROM pos_payment AS payment, pos_payment_method AS method WHERE payment.payment_method_id = method.id AND payment.id IN %s GROUP BY method.name """, (tuple(payment_ids),)) payments = self.env.cr.dictfetchall() else: payments = [] return { 'currency_precision': user_currency.decimal_places, 'total_paid': user_currency.round(total), 'payments': payments, 'company_name': self.env.company.name, 'taxes': list(taxes.values()), 'products': sorted([{ 'product_id': product.id, 'product_name': product.name, 'code': product.default_code, 'quantity': qty, 'price_unit': price_unit, 'discount': discount, 'uom': product.uom_id.name } for (product, price_unit, discount), qty in products_sold.items()], key=lambda l: l['product_name']) }
def _compute_amounts(self, frequency, company): """ Method used to compute all the business data of the new object. It will search for previous closings of the same frequency to infer the move from which account move lines should be fetched. @param {string} frequency: a valid value of the selection field on the object (daily, monthly, annually) frequencies are literal (daily means 24 hours and so on) @param {recordset} company: the company for which the closing is done @return {dict} containing {field: value} for each business field of the object """ interval_dates = self._interval_dates(frequency, company) previous_closing = self.search([('frequency', '=', frequency), ('company_id', '=', company.id)], limit=1, order='sequence_number desc') first_order = self.env['pos.order'] date_start = interval_dates['interval_from'] cumulative_total = 0 if previous_closing: first_order = previous_closing.last_order_id date_start = previous_closing.create_date cumulative_total += previous_closing.cumulative_total domain = [('company_id', '=', company.id), ('state', 'in', ('paid', 'done', 'invoiced'))] if first_order.l10n_fr_secure_sequence_number is not False and first_order.l10n_fr_secure_sequence_number is not None: domain = AND([ domain, [('l10n_fr_secure_sequence_number', '>', first_order.l10n_fr_secure_sequence_number)] ]) elif date_start: #the first time we compute the closing, we consider only from the installation of the module domain = AND([domain, [('date_order', '>=', date_start)]]) orders = self.env['pos.order'].search(domain, order='date_order desc') total_interval = sum(orders.mapped('amount_total')) cumulative_total += total_interval # We keep the reference to avoid gaps (like daily object during the weekend) last_order = first_order if orders: last_order = orders[0] return { 'total_interval': total_interval, 'cumulative_total': cumulative_total, 'last_order_id': last_order.id, 'last_order_hash': last_order.l10n_fr_secure_sequence_number, 'date_closing_stop': interval_dates['date_stop'], 'date_closing_start': date_start, 'name': interval_dates['name_interval'] + ' - ' + interval_dates['date_stop'][:10] }
def portal_my_timesheets(self, page=1, sortby=None, filterby=None, search=None, search_in='all', groupby='none', **kw): Timesheet_sudo = request.env['account.analytic.line'].sudo() values = self._prepare_portal_layout_values() domain = request.env['account.analytic.line']._timesheet_get_portal_domain() _items_per_page = 100 searchbar_sortings = { 'date': {'label': _('Newest'), 'order': 'date desc'}, 'name': {'label': _('Description'), 'order': 'name'}, } searchbar_inputs = self._get_searchbar_inputs() searchbar_groupby = self._get_searchbar_groupby() today = fields.Date.today() quarter_start, quarter_end = date_utils.get_quarter(today) last_week = today + relativedelta(weeks=-1) last_month = today + relativedelta(months=-1) last_year = today + relativedelta(years=-1) searchbar_filters = { 'all': {'label': _('All'), 'domain': []}, 'today': {'label': _('Today'), 'domain': [("date", "=", today)]}, 'week': {'label': _('This week'), 'domain': [('date', '>=', date_utils.start_of(today, "week")), ('date', '<=', date_utils.end_of(today, 'week'))]}, 'month': {'label': _('This month'), 'domain': [('date', '>=', date_utils.start_of(today, 'month')), ('date', '<=', date_utils.end_of(today, 'month'))]}, 'year': {'label': _('This year'), 'domain': [('date', '>=', date_utils.start_of(today, 'year')), ('date', '<=', date_utils.end_of(today, 'year'))]}, 'quarter': {'label': _('This Quarter'), 'domain': [('date', '>=', quarter_start), ('date', '<=', quarter_end)]}, 'last_week': {'label': _('Last week'), 'domain': [('date', '>=', date_utils.start_of(last_week, "week")), ('date', '<=', date_utils.end_of(last_week, 'week'))]}, 'last_month': {'label': _('Last month'), 'domain': [('date', '>=', date_utils.start_of(last_month, 'month')), ('date', '<=', date_utils.end_of(last_month, 'month'))]}, 'last_year': {'label': _('Last year'), 'domain': [('date', '>=', date_utils.start_of(last_year, 'year')), ('date', '<=', date_utils.end_of(last_year, 'year'))]}, } # default sort by value if not sortby: sortby = 'date' order = searchbar_sortings[sortby]['order'] # default filter by value if not filterby: filterby = 'all' domain = AND([domain, searchbar_filters[filterby]['domain']]) if search and search_in: domain += self._get_search_domain(search_in, search) timesheet_count = Timesheet_sudo.search_count(domain) # pager pager = portal_pager( url="/my/timesheets", url_args={'sortby': sortby, 'search_in': search_in, 'search': search, 'filterby': filterby, 'groupby': groupby}, total=timesheet_count, page=page, step=_items_per_page ) def get_timesheets(): groupby_mapping = self._get_groupby_mapping() field = groupby_mapping.get(groupby, None) orderby = '%s, %s' % (field, order) if field else order timesheets = Timesheet_sudo.search(domain, order=orderby, limit=_items_per_page, offset=pager['offset']) if field: if groupby == 'date': time_data = Timesheet_sudo.read_group(domain, ['date', 'unit_amount:sum'], ['date:day']) mapped_time = dict([(datetime.strptime(m['date:day'], '%d %b %Y').date(), m['unit_amount']) for m in time_data]) grouped_timesheets = [(Timesheet_sudo.concat(*g), mapped_time[k]) for k, g in groupbyelem(timesheets, itemgetter('date'))] else: time_data = time_data = Timesheet_sudo.read_group(domain, [field, 'unit_amount:sum'], [field]) mapped_time = dict([(m[field][0] if m[field] else False, m['unit_amount']) for m in time_data]) grouped_timesheets = [(Timesheet_sudo.concat(*g), mapped_time[k.id]) for k, g in groupbyelem(timesheets, itemgetter(field))] return timesheets, grouped_timesheets grouped_timesheets = [( timesheets, sum(Timesheet_sudo.search(domain).mapped('unit_amount')) )] if timesheets else [] return timesheets, grouped_timesheets timesheets, grouped_timesheets = get_timesheets() values.update({ 'timesheets': timesheets, 'grouped_timesheets': grouped_timesheets, 'page_name': 'timesheet', 'default_url': '/my/timesheets', 'pager': pager, 'searchbar_sortings': searchbar_sortings, 'search_in': search_in, 'search': search, 'sortby': sortby, 'groupby': groupby, 'searchbar_inputs': searchbar_inputs, 'searchbar_groupby': searchbar_groupby, 'searchbar_filters': OrderedDict(sorted(searchbar_filters.items())), 'filterby': filterby, 'is_uom_day': request.env['account.analytic.line']._is_timesheet_encode_uom_day(), }) return request.render("hr_timesheet.portal_my_timesheets", values)
def _import_fattura_pa(self, tree, invoice): """ Decodes a fattura_pa invoice into an invoice. :param tree: the fattura_pa tree to decode. :param invoice: the invoice to update or an empty recordset. :returns: the invoice where the fattura_pa data was imported. """ invoices = self.env['account.move'] first_run = True # possible to have multiple invoices in the case of an invoice batch, the batch itself is repeated for every invoice of the batch for body_tree in tree.xpath('//FatturaElettronicaBody'): if not first_run or not invoice: # make sure all the iterations create a new invoice record (except the first which could have already created one) invoice = self.env['account.move'] first_run = False # Type must be present in the context to get the right behavior of the _default_journal method (account.move). # journal_id must be present in the context to get the right behavior of the _default_account method (account.move.line). elements = tree.xpath('//CessionarioCommittente//IdCodice') company = elements and self.env['res.company'].search( [('vat', 'ilike', elements[0].text)], limit=1) if not company: elements = tree.xpath( '//CessionarioCommittente//CodiceFiscale') company = elements and self.env['res.company'].search( [('l10n_it_codice_fiscale', 'ilike', elements[0].text)], limit=1) if not company: # Only invoices with a correct VAT or Codice Fiscale can be imported _logger.warning( 'No company found with VAT or Codice Fiscale like %r.', elements[0].text) continue # Refund type. # TD01 == invoice # TD02 == advance/down payment on invoice # TD03 == advance/down payment on fee # TD04 == credit note # TD05 == debit note # TD06 == fee # For unsupported document types, just assume in_invoice, and log that the type is unsupported elements = tree.xpath('//DatiGeneraliDocumento/TipoDocumento') move_type = 'in_invoice' if elements and elements[0].text and elements[0].text == 'TD04': move_type = 'in_refund' elif elements and elements[0].text and elements[0].text != 'TD01': _logger.info( 'Document type not managed: %s. Invoice type is set by default.', elements[0].text) # Setup the context for the Invoice Form invoice_ctx = invoice.with_company(company) \ .with_context(default_move_type=move_type) # move could be a single record (editing) or be empty (new). with Form(invoice_ctx) as invoice_form: message_to_log = [] # Partner (first step to avoid warning 'Warning! You must first select a partner.'). <1.2> elements = tree.xpath('//CedentePrestatore//IdCodice') partner = elements and self.env['res.partner'].search([ '&', ('vat', 'ilike', elements[0].text), '|', ('company_id', '=', company.id), ('company_id', '=', False) ], limit=1) if not partner: elements = tree.xpath('//CedentePrestatore//CodiceFiscale') if elements: codice = elements[0].text domains = [[('l10n_it_codice_fiscale', '=', codice)]] if re.match(r'^[0-9]{11}$', codice): domains.append([('l10n_it_codice_fiscale', '=', 'IT' + codice)]) elif re.match(r'^IT[0-9]{11}$', codice): domains.append([ ('l10n_it_codice_fiscale', '=', self.env['res.partner']. _l10n_it_normalize_codice_fiscale(codice)) ]) partner = elements and self.env['res.partner'].search( AND([ OR(domains), OR([[('company_id', '=', company.id)], [('company_id', '=', False)]]) ]), limit=1) if not partner: elements = tree.xpath('//DatiTrasmissione//Email') partner = elements and self.env['res.partner'].search( [ '&', '|', ('email', '=', elements[0].text), ('l10n_it_pec_email', '=', elements[0].text), '|', ('company_id', '=', company.id), ('company_id', '=', False) ], limit=1) if partner: invoice_form.partner_id = partner else: message_to_log.append("%s<br/>%s" % ( _("Vendor not found, useful informations from XML file:" ), invoice._compose_info_message(tree, './/CedentePrestatore'))) # Numbering attributed by the transmitter. <1.1.2> elements = tree.xpath('//ProgressivoInvio') if elements: invoice_form.payment_reference = elements[0].text elements = body_tree.xpath('.//DatiGeneraliDocumento//Numero') if elements: invoice_form.ref = elements[0].text # Currency. <2.1.1.2> elements = body_tree.xpath('.//DatiGeneraliDocumento/Divisa') if elements: currency_str = elements[0].text currency = self.env.ref('base.%s' % currency_str.upper(), raise_if_not_found=False) if currency != self.env.company.currency_id and currency.active: invoice_form.currency_id = currency # Date. <2.1.1.3> elements = body_tree.xpath('.//DatiGeneraliDocumento/Data') if elements: date_str = elements[0].text date_obj = datetime.strptime( date_str, DEFAULT_FACTUR_ITALIAN_DATE_FORMAT) invoice_form.invoice_date = date_obj # Dati Bollo. <2.1.1.6> elements = body_tree.xpath( './/DatiGeneraliDocumento/DatiBollo/ImportoBollo') if elements: invoice_form.l10n_it_stamp_duty = float(elements[0].text) # List of all amount discount (will be add after all article to avoid to have a negative sum) discount_list = [] percentage_global_discount = 1.0 # Global discount. <2.1.1.8> discount_elements = body_tree.xpath( './/DatiGeneraliDocumento/ScontoMaggiorazione') total_discount_amount = 0.0 if discount_elements: for discount_element in discount_elements: discount_line = discount_element.xpath('.//Tipo') discount_sign = -1 if discount_line and discount_line[0].text == 'SC': discount_sign = 1 discount_percentage = discount_element.xpath( './/Percentuale') if discount_percentage and discount_percentage[0].text: percentage_global_discount *= 1 - float( discount_percentage[0].text ) / 100 * discount_sign discount_amount_text = discount_element.xpath( './/Importo') if discount_amount_text and discount_amount_text[ 0].text: discount_amount = float(discount_amount_text[0]. text) * discount_sign * -1 discount = {} discount["seq"] = 0 if discount_amount < 0: discount["name"] = _('GLOBAL DISCOUNT') else: discount["name"] = _('GLOBAL EXTRA CHARGE') discount["amount"] = discount_amount discount["tax"] = [] discount_list.append(discount) # Comment. <2.1.1.11> elements = body_tree.xpath('.//DatiGeneraliDocumento//Causale') for element in elements: invoice_form.narration = '%s%s\n' % (invoice_form.narration or '', element.text) # Informations relative to the purchase order, the contract, the agreement, # the reception phase or invoices previously transmitted # <2.1.2> - <2.1.6> for document_type in [ 'DatiOrdineAcquisto', 'DatiContratto', 'DatiConvenzione', 'DatiRicezione', 'DatiFattureCollegate' ]: elements = body_tree.xpath('.//DatiGenerali/' + document_type) if elements: for element in elements: message_to_log.append( "%s %s<br/>%s" % (document_type, _("from XML file:"), invoice._compose_info_message(element, '.'))) # Dati DDT. <2.1.8> elements = body_tree.xpath('.//DatiGenerali/DatiDDT') if elements: message_to_log.append( "%s<br/>%s" % (_("Transport informations from XML file:"), invoice._compose_info_message( body_tree, './/DatiGenerali/DatiDDT'))) # Due date. <2.4.2.5> elements = body_tree.xpath( './/DatiPagamento/DettaglioPagamento/DataScadenzaPagamento' ) if elements: date_str = elements[0].text date_obj = datetime.strptime( date_str, DEFAULT_FACTUR_ITALIAN_DATE_FORMAT) invoice_form.invoice_date_due = fields.Date.to_string( date_obj) # Total amount. <2.4.2.6> elements = body_tree.xpath('.//ImportoPagamento') amount_total_import = 0 for element in elements: amount_total_import += float(element.text) if amount_total_import: message_to_log.append( _("Total amount from the XML File: %s") % (amount_total_import)) # Bank account. <2.4.2.13> if invoice_form.move_type not in ('out_invoice', 'in_refund'): elements = body_tree.xpath( './/DatiPagamento/DettaglioPagamento/IBAN') if elements: if invoice_form.partner_id and invoice_form.partner_id.commercial_partner_id: bank = self.env['res.partner.bank'].search([ ('acc_number', '=', elements[0].text), ('partner_id.id', '=', invoice_form.partner_id. commercial_partner_id.id) ]) else: bank = self.env['res.partner.bank'].search([ ('acc_number', '=', elements[0].text) ]) if bank: invoice_form.partner_bank_id = bank else: message_to_log.append("%s<br/>%s" % ( _("Bank account not found, useful informations from XML file:" ), invoice._compose_multi_info_message( body_tree, [ './/DatiPagamento//Beneficiario', './/DatiPagamento//IstitutoFinanziario', './/DatiPagamento//IBAN', './/DatiPagamento//ABI', './/DatiPagamento//CAB', './/DatiPagamento//BIC', './/DatiPagamento//ModalitaPagamento' ]))) else: elements = body_tree.xpath( './/DatiPagamento/DettaglioPagamento') if elements: message_to_log.append("%s<br/>%s" % ( _("Bank account not found, useful informations from XML file:" ), invoice._compose_info_message( body_tree, './/DatiPagamento'))) # Invoice lines. <2.2.1> elements = body_tree.xpath('.//DettaglioLinee') if elements: for element in elements: with invoice_form.invoice_line_ids.new( ) as invoice_line_form: # Sequence. line_elements = element.xpath('.//NumeroLinea') if line_elements: invoice_line_form.sequence = int( line_elements[0].text) * 2 # Product. line_elements = element.xpath('.//Descrizione') if line_elements: invoice_line_form.name = " ".join( line_elements[0].text.split()) elements_code = element.xpath('.//CodiceArticolo') if elements_code: for element_code in elements_code: type_code = element_code.xpath( './/CodiceTipo')[0] code = element_code.xpath( './/CodiceValore')[0] if type_code.text == 'EAN': product = self.env[ 'product.product'].search([ ('barcode', '=', code.text) ]) if product: invoice_line_form.product_id = product break if partner: product_supplier = self.env[ 'product.supplierinfo'].search([ ('name', '=', partner.id), ('product_code', '=', code.text) ]) if product_supplier and product_supplier.product_id: invoice_line_form.product_id = product_supplier.product_id break if not invoice_line_form.product_id: for element_code in elements_code: code = element_code.xpath( './/CodiceValore')[0] product = self.env[ 'product.product'].search( [('default_code', '=', code.text)], limit=1) if product: invoice_line_form.product_id = product break # Price Unit. line_elements = element.xpath('.//PrezzoUnitario') if line_elements: invoice_line_form.price_unit = float( line_elements[0].text) # Quantity. line_elements = element.xpath('.//Quantita') if line_elements: invoice_line_form.quantity = float( line_elements[0].text) else: invoice_line_form.quantity = 1 # Taxes tax_element = element.xpath('.//AliquotaIVA') natura_element = element.xpath('.//Natura') invoice_line_form.tax_ids.clear() if tax_element and tax_element[0].text: percentage = float(tax_element[0].text) if natura_element and natura_element[0].text: l10n_it_kind_exoneration = natura_element[ 0].text tax = self.env['account.tax'].search( [ ('company_id', '=', invoice_form.company_id.id), ('amount_type', '=', 'percent'), ('type_tax_use', '=', 'purchase'), ('amount', '=', percentage), ('l10n_it_kind_exoneration', '=', l10n_it_kind_exoneration), ], limit=1) else: tax = self.env['account.tax'].search( [ ('company_id', '=', invoice_form.company_id.id), ('amount_type', '=', 'percent'), ('type_tax_use', '=', 'purchase'), ('amount', '=', percentage), ], limit=1) l10n_it_kind_exoneration = '' if tax: invoice_line_form.tax_ids.add(tax) else: if l10n_it_kind_exoneration: message_to_log.append( _("Tax not found with percentage: %s and exoneration %s for the article: %s" ) % (percentage, l10n_it_kind_exoneration, invoice_line_form.name)) else: message_to_log.append( _("Tax not found with percentage: %s for the article: %s" ) % (percentage, invoice_line_form.name)) # Discount in cascade mode. # if 3 discounts : -10% -50€ -20% # the result must be : (((price -10%)-50€) -20%) # Generic form : (((price -P1%)-A1€) -P2%) # It will be split in two parts: fix amount and pourcent amount # example: (((((price - A1€) -P2%) -A3€) -A4€) -P5€) # pourcent: 1-(1-P2)*(1-P5) # fix amount: A1*(1-P2)*(1-P5)+A3*(1-P5)+A4*(1-P5) (we must take account of all # percentage present after the fix amount) line_elements = element.xpath( './/ScontoMaggiorazione') total_discount_amount = 0.0 total_discount_percentage = percentage_global_discount if line_elements: for line_element in line_elements: discount_line = line_element.xpath( './/Tipo') discount_sign = -1 if discount_line and discount_line[ 0].text == 'SC': discount_sign = 1 discount_percentage = line_element.xpath( './/Percentuale') if discount_percentage and discount_percentage[ 0].text: pourcentage_actual = 1 - float( discount_percentage[0].text ) / 100 * discount_sign total_discount_percentage *= pourcentage_actual total_discount_amount *= pourcentage_actual discount_amount = line_element.xpath( './/Importo') if discount_amount and discount_amount[ 0].text: total_discount_amount += float( discount_amount[0].text ) * discount_sign * -1 # Save amount discount. if total_discount_amount != 0: discount = {} discount[ "seq"] = invoice_line_form.sequence + 1 if total_discount_amount < 0: discount["name"] = _( 'DISCOUNT: %s', invoice_line_form.name) else: discount["name"] = _( 'EXTRA CHARGE: %s', invoice_line_form.name) discount["amount"] = total_discount_amount discount["tax"] = [] for tax in invoice_line_form.tax_ids: discount["tax"].append(tax) discount_list.append(discount) invoice_line_form.discount = ( 1 - total_discount_percentage) * 100 # Apply amount discount. for discount in discount_list: with invoice_form.invoice_line_ids.new( ) as invoice_line_form_discount: invoice_line_form_discount.tax_ids.clear() invoice_line_form_discount.sequence = discount["seq"] invoice_line_form_discount.name = discount["name"] invoice_line_form_discount.price_unit = discount[ "amount"] new_invoice = invoice_form.save() new_invoice.l10n_it_send_state = "other" elements = body_tree.xpath('.//Allegati') if elements: for element in elements: name_attachment = element.xpath( './/NomeAttachment')[0].text attachment_64 = str.encode( element.xpath('.//Attachment')[0].text) attachment_64 = self.env['ir.attachment'].create({ 'name': name_attachment, 'datas': attachment_64, 'type': 'binary', }) # default_res_id is had to context to avoid facturx to import his content # no_new_invoice to prevent from looping on the message_post that would create a new invoice without it new_invoice.with_context( no_new_invoice=True, default_res_id=new_invoice.id).message_post( body=(_("Attachment from XML")), attachment_ids=[attachment_64.id]) for message in message_to_log: new_invoice.message_post(body=message) invoices += new_invoice return invoices
def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True): """ This is a hack to allow us to correctly calculate the average of PO specific date values since the normal report query result will duplicate PO values across its PO lines during joins and lead to incorrect aggregation values. Only the AVG operator is supported for avg_days_to_purchase. """ avg_days_to_purchase = next( (field for field in fields if re.search(r'\bavg_days_to_purchase\b', field)), False) if avg_days_to_purchase: fields.remove(avg_days_to_purchase) if any( field.split(':')[1].split('(')[0] != 'avg' for field in [avg_days_to_purchase] if field): raise UserError( "Value: 'avg_days_to_purchase' should only be used to show an average. If you are seeing this message then it is being accessed incorrectly." ) res = [] if fields: res = super(PurchaseReport, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy) if not res and avg_days_to_purchase: res = [{}] if avg_days_to_purchase: self.check_access_rights('read') query = """ SELECT AVG(days_to_purchase.po_days_to_purchase)::decimal(16,2) AS avg_days_to_purchase FROM ( SELECT extract(epoch from age(po.date_approve,po.create_date))/(24*60*60) AS po_days_to_purchase FROM purchase_order po WHERE po.id IN ( SELECT "purchase_report"."order_id" FROM %s WHERE %s) ) AS days_to_purchase """ subdomain = AND([ domain, [('company_id', '=', self.env.company.id), ('date_approve', '!=', False)] ]) subtables, subwhere, subparams = expression(subdomain, self).query.get_sql() self.env.cr.execute(query % (subtables, subwhere), subparams) res[0].update({ '__count': 1, avg_days_to_purchase.split(':')[0]: self.env.cr.fetchall()[0][0], }) return res