From 4b432ec207ebfbaac3e6a3c7a0ed365d466381e8 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Tue, 22 Sep 2020 15:25:54 +0200 Subject: [PATCH] stock_valuation_xlsx: add possibility to add custom products fields in report --- .../wizard/stock_valuation_xlsx.py | 279 ++++++++++-------- .../wizard/stock_valuation_xlsx_view.xml | 2 +- 2 files changed, 153 insertions(+), 128 deletions(-) diff --git a/stock_valuation_xlsx/wizard/stock_valuation_xlsx.py b/stock_valuation_xlsx/wizard/stock_valuation_xlsx.py index f389ed2..4655213 100644 --- a/stock_valuation_xlsx/wizard/stock_valuation_xlsx.py +++ b/stock_valuation_xlsx/wizard/stock_valuation_xlsx.py @@ -71,7 +71,7 @@ class StockValuationXlsx(models.TransientModel): if self.warehouse_id: self.location_id = self.warehouse_id.view_location_id.id - def _check_config(self): + def _check_config(self, company_id): self.ensure_one() if ( self.source == 'stock' and @@ -85,7 +85,18 @@ class StockValuationXlsx(models.TransientModel): raise UserError(_( "The selected inventory (%s) is not in done state.") % self.inventory_id.display_name) - # raise si la valuation method est real + cost_method_real_count = self.env['ir.property'].search([ + ('company_id', '=', company_id), + ('name', '=', 'property_cost_method'), + ('value_text', '=', 'real'), + ('type', '=', 'selection'), + ], count=True) + if cost_method_real_count: + raise UserError(_( + "There are %d properties that have " + "'Costing Method' = 'Real Price'. This costing " + "method is not supported by this module.") + % cost_method_real_count) def _prepare_product_domain(self): self.ensure_one() @@ -94,35 +105,42 @@ class StockValuationXlsx(models.TransientModel): domain += [('categ_id', 'child_of', self.categ_ids.ids)] return domain + def _prepare_product_fields(self): + return ['uom_id', 'name', 'default_code', 'categ_id'] + def compute_product_data(self, company_id, past_date=False): self.ensure_one() logger.debug('Start compute_product_data') ppo = self.env['product.product'] ppho = self.env['product.price.history'] domain = self._prepare_product_domain() - products = ppo.search_read( - domain, ['uom_id', 'name', 'default_code', 'categ_id']) + fields_list = self._prepare_product_fields() + if not past_date: + fields_list.append('standard_price') + products = ppo.search_read(domain, fields_list) product_id2data = {} now = fields.Datetime.now() for p in products: logger.debug('p=%d', p['id']) # I don't call the native method get_history_price() # because it requires a browse record and it is too slow - history = ppho.search_read([ - ('company_id', '=', company_id), - ('product_id', '=', p['id']), - ('datetime', '<=', past_date or now)], - ['cost'], order='datetime desc, id desc', limit=1) - standard_price = history and history[0]['cost'] or 0.0 - product_id2data[p['id']] = { - 'default_code': p['default_code'], - 'name': p['name'], - 'categ_id': p['categ_id'][0], - 'uom_id': p['uom_id'][0], - 'standard_price': standard_price, - } + if past_date: + history = ppho.search_read([ + ('company_id', '=', company_id), + ('product_id', '=', p['id']), + ('datetime', '<=', past_date or now)], + ['cost'], order='datetime desc, id desc', limit=1) + standard_price = history and history[0]['cost'] or 0.0 + else: + standard_price = p['standard_price'] + product_id2data[p['id']] = {'standard_price': standard_price} + for pfield in fields_list: + if pfield.endswith('_id'): + product_id2data[p['id']][pfield] = p[pfield][0] + else: + product_id2data[p['id']][pfield] = p[pfield] logger.debug('End compute_product_data') - return product_id2data, product_id2data.keys() + return product_id2data def id2name(self, product_ids): logger.debug('Start id2name') @@ -145,11 +163,13 @@ class StockValuationXlsx(models.TransientModel): if hasattr(splo, 'expiry_date'): lot_fields.append('expiry_date') - lots = splo.search_read([('product_id', 'in', product_ids)], lot_fields) + lots = splo.search_read( + [('product_id', 'in', product_ids)], lot_fields) for lot in lots: lot_id2data[lot['id']] = lot loc_id2name = {} - locs = slo.search_read([('id', 'child_of', self.location_id.id)], ['display_name']) + locs = slo.search_read( + [('id', 'child_of', self.location_id.id)], ['display_name']) for loc in locs: loc_id2name[loc['id']] = loc['display_name'] logger.debug('End id2name') @@ -199,7 +219,7 @@ class StockValuationXlsx(models.TransientModel): def compute_data_from_past_stock(self, product_ids, prec_qty, past_date): self.ensure_one() - logger.debug('Start compute_data_from_past_stock past_date=', past_date) + logger.debug('Start compute_data_from_past_stock past_date=%s', past_date) ppo = self.env['product.product'] products = ppo.with_context(to_date=past_date, location_id=self.location_id.id).browse(product_ids) res = [] @@ -247,23 +267,18 @@ class StockValuationXlsx(models.TransientModel): precision_digits=prec_price) subtotal = float_round( standard_price * qty, precision_rounding=prec_cur_rounding) - expiry_date_dt = '' - if l['lot_id'] and lot_id2data[l['lot_id']]['expiry_date']: - expiry_date_dt = fields.Date.from_string( - lot_id2data[l['lot_id']]['expiry_date']) - res.append({ - 'product_code': product_id2data[product_id]['default_code'], - 'product_name': product_id2data[product_id]['name'], - 'loc_name': l['location_id'] and loc_id2name[l['location_id']] or '', - 'lot_name': l['lot_id'] and lot_id2data[l['lot_id']]['name'] or '', - 'expiry_date': expiry_date_dt, - 'qty': qty, - 'uom_name': uom_id2name[product_id2data[product_id]['uom_id']], - 'standard_price': standard_price, - 'subtotal': subtotal, - 'categ_name': categ_id2name[product_id2data[product_id]['categ_id']], - 'categ_id': categ_subtotal and product_id2data[product_id]['categ_id'] or 0, - }) + res.append(dict( + product_id2data[product_id], + product_name=product_id2data[product_id]['name'], + loc_name=l['location_id'] and loc_id2name[l['location_id']] or '', + lot_name=l['lot_id'] and lot_id2data[l['lot_id']]['name'] or '', + expiry_date=l['lot_id'] and lot_id2data[l['lot_id']].get('expiry_date'), + qty=qty, + uom_name=uom_id2name[product_id2data[product_id]['uom_id']], + standard_price=standard_price, + subtotal=subtotal, + categ_name=categ_id2name[product_id2data[product_id]['categ_id']], + )) sort_res = sorted(res, key=lambda x: x['product_name']) logger.debug('End stringify_and_sort_result') return sort_res @@ -273,12 +288,12 @@ class StockValuationXlsx(models.TransientModel): logger.debug('Start generate XLSX stock valuation report') splo = self.env['stock.production.lot'].with_context(active_test=False) pco = self.env['product.category'].with_context(active_test=False) - self._check_config() prec_qty = self.env['decimal.precision'].precision_get('Product Unit of Measure') prec_price = self.env['decimal.precision'].precision_get('Product Price') company = self.env.user.company_id company_id = company.id prec_cur_rounding = company.currency_id.rounding + self._check_config(company_id) split_by_lot = self.split_by_lot split_by_location = self.split_by_location @@ -287,8 +302,11 @@ class StockValuationXlsx(models.TransientModel): split_by_lot = False split_by_location = False past_date = self.past_date - product_id2data, product_ids = self.compute_product_data( + elif self.source == 'inventory': + past_date = self.inventory_id.date + product_id2data = self.compute_product_data( company_id, past_date=past_date) + product_ids = product_id2data.keys() if self.source == 'stock': if self.stock_date_type == 'present': data = self.compute_data_from_present_stock( @@ -304,81 +322,32 @@ class StockValuationXlsx(models.TransientModel): product_ids, product_id2data, data_res, prec_qty, prec_price, prec_cur_rounding, categ_id2name, uom_id2name, lot_id2data, loc_id2name) - logger.debug('Start create XLSX workbook') file_data = StringIO() workbook = xlsxwriter.Workbook(file_data) sheet = workbook.add_worksheet('Stock') - # STYLES - total_bg_color = '#faa03a' - categ_bg_color = '#e1daf5' - col_title_bg_color = '#fff9b4' - regular_font_size = 10 - currency_num_format = u'# ### ##0.00 %s' % company.currency_id.symbol - price_currency_num_format = u'# ### ##0.%s %s' % ('0' * prec_price, company.currency_id.symbol) - doc_title = workbook.add_format({ - 'bold': True, 'font_size': regular_font_size + 10, - 'font_color': '#003b6f'}) - doc_subtitle = workbook.add_format({ - 'bold': True, 'font_size': regular_font_size}) - col_title = workbook.add_format({ - 'bold': True, 'bg_color': col_title_bg_color, - 'text_wrap': True, 'font_size': regular_font_size, - 'align': 'center', - }) - total_title = workbook.add_format({ - 'bold': True, 'text_wrap': True, 'font_size': regular_font_size + 2, - 'align': 'right', 'bg_color': total_bg_color}) - total_currency = workbook.add_format({ - 'num_format': currency_num_format, 'bg_color': total_bg_color}) - regular_date = workbook.add_format({'num_format': 'dd/mm/yyyy'}) - regular_currency = workbook.add_format({'num_format': currency_num_format}) - regular_price_currency = workbook.add_format({'num_format': price_currency_num_format}) - regular = workbook.add_format({}) - regular_small = workbook.add_format({'font_size': regular_font_size - 2}) - categ_title = workbook.add_format({ - 'bold': True, 'bg_color': categ_bg_color, 'font_size': regular_font_size}) - categ_currency = workbook.add_format({ - 'num_format': currency_num_format, 'bg_color': categ_bg_color}) - date_title = workbook.add_format({ - 'bold': True, 'font_size': regular_font_size, 'align': 'right'}) - date_title_val = workbook.add_format({ - 'bold': True, 'font_size': regular_font_size}) - - cols = { - 'product_code': {'width': 18, 'style': regular, 'pos': -1, 'title': _('Product Code')}, - 'product_name': {'width': 40, 'style': regular, 'pos': -1, 'title': _('Product Name')}, - 'loc_name': {'width': 25, 'style': regular_small, 'pos': -1, 'title': _('Location Name')}, - 'lot_name': {'width': 18, 'style': regular, 'pos': -1, 'title': _('Lot')}, - 'expiry_date': {'width': 11, 'style': regular_date, 'pos': -1, 'title': _('Expiry Date')}, - 'qty': {'width': 8, 'style': regular, 'pos': -1, 'title': _('Qty')}, - 'uom_name': {'width': 5, 'style': regular_small, 'pos': -1, 'title': _('UoM')}, - 'standard_price': {'width': 10, 'style': regular_price_currency, 'pos': -1, 'title': _('Price')}, - 'subtotal': {'width': 16, 'style': regular_currency, 'pos': -1, 'title': _('Sub-total'), 'formula': True}, - 'categ_subtotal': {'width': 16, 'style': regular_currency, 'pos': -1, 'title': _('Categ Sub-total'), 'formula': True}, - 'categ_name': {'width': 35, 'style': regular_small, 'pos': -1, 'title': _('Product Category')}, - } + styles = self._prepare_styles(workbook, company, prec_price) + cols = self._prepare_cols() categ_subtotal = self.categ_subtotal - col_order = [ - 'product_code', - 'product_name', - split_by_location and 'loc_name' or False, - split_by_lot and 'lot_name' or False, - split_by_lot and hasattr(splo, 'expiry_date') and 'expiry_date' or False, - 'qty', - 'uom_name', - 'standard_price', - 'subtotal', - categ_subtotal and 'categ_subtotal' or 'categ_name', - ] + # remove cols that we won't use + if not split_by_lot: + cols.pop('lot_name', None) + cols.pop('expiry_date', None) + if not hasattr(splo, 'expiry_date'): + cols.pop('expiry_date', None) + if not split_by_location: + cols.pop('loc_name', None) + if not categ_subtotal: + cols.pop('categ_subtotal', None) + tmp_list = sorted(cols.items(), key=lambda x: x[1]['sequence']) + col_sorted = [x[0] for x in tmp_list] j = 0 - for col in col_order: - if col: - cols[col]['pos'] = j - cols[col]['pos_letter'] = chr(j + 97).upper() - sheet.set_column(j, j, cols[col]['width']) - j += 1 + for col, col_vals in sorted(cols.items(), key=lambda x: x[1]['sequence']): + cols[col]['pos'] = j + cols[col]['pos_letter'] = chr(j + 97).upper() + sheet.set_column(j, j, cols[col]['width']) + j += 1 # HEADER if past_date: @@ -388,25 +357,23 @@ class StockValuationXlsx(models.TransientModel): stock_time_dt = fields.Datetime.context_timestamp(self, datetime.now()) stock_time_str = fields.Datetime.to_string(stock_time_dt) i = 0 - sheet.write(i, 0, 'Odoo - Stock Valuation', doc_title) + sheet.write(i, 0, 'Odoo - Stock Valuation', styles['doc_title']) sheet.set_row(0, 26) i += 1 - sheet.write(i, 0, 'Valuation Date: %s' % stock_time_str, doc_subtitle) -# sheet.write(i, 3, stock_time_str, date_title_val) + sheet.write(i, 0, 'Valuation Date: %s' % stock_time_str, styles['doc_subtitle']) i += 1 - sheet.write(i, 0, 'Stock location (children included): %s' % self.location_id.complete_name, doc_subtitle) + sheet.write(i, 0, 'Stock location (children included): %s' % self.location_id.complete_name, styles['doc_subtitle']) if self.categ_ids: i += 1 - sheet.write(i, 0, 'Product Categories: %s' % ', '.join([categ.display_name for categ in self.categ_ids]), doc_subtitle) + sheet.write(i, 0, 'Product Categories: %s' % ', '.join([categ.display_name for categ in self.categ_ids]), styles['doc_subtitle']) # TITLE of COLS i += 2 for col in cols.values(): - if col['pos'] >= 0: - sheet.write(i, col['pos'], col['title'], col_title) + sheet.write(i, col['pos'], col['title'], styles['col_title']) i += 1 - sheet.write(i, cols['subtotal']['pos'] - 1, _("TOTAL:"), total_title) + sheet.write(i, cols['subtotal']['pos'] - 1, _("TOTAL:"), styles['total_title']) total_row = i # LINES @@ -424,33 +391,35 @@ class StockValuationXlsx(models.TransientModel): ctotal = 0.0 categ_has_line = False if categ_subtotal: + # skip a line and save it's position as crow i += 1 crow = i - sheet.write(crow, 0, categ_id2name[categ_id], categ_title) - for x in range(cols['categ_subtotal']['pos'] - 1): - sheet.write(crow, x + 1, '', categ_title) for l in filter(lambda x: x['categ_id'] == categ_id, res): i += 1 total += l['subtotal'] ctotal += l['subtotal'] categ_has_line = True subtotal_formula = '=%s%d*%s%d' % (letter_qty, i + 1, letter_price, i + 1) - sheet.write_formula(i, cols['subtotal']['pos'], subtotal_formula, regular_currency, l['subtotal']) + sheet.write_formula(i, cols['subtotal']['pos'], subtotal_formula, styles['regular_currency'], l['subtotal']) for col_name, col in cols.items(): - if col['pos'] >= 0 and not col.get('formula'): - sheet.write(i, col['pos'], l[col_name], col['style']) + if not col.get('formula'): + if col.get('type') == 'date' and l[col_name]: + l[col_name] = fields.Date.from_string(l[col_name]) + sheet.write(i, col['pos'], l[col_name], styles[col['style']]) if categ_subtotal: if categ_has_line: + sheet.write(crow, 0, categ_id2name[categ_id], styles['categ_title']) + for x in range(cols['categ_subtotal']['pos'] - 1): + sheet.write(crow, x + 1, '', styles['categ_title']) + cformula = '=SUM(%s%d:%s%d)' % (letter_subtotal, crow + 2, letter_subtotal, i + 1) - sheet.write_formula(crow, cols['categ_subtotal']['pos'], cformula, categ_currency, float_round(ctotal, precision_rounding=prec_cur_rounding)) + sheet.write_formula(crow, cols['categ_subtotal']['pos'], cformula, styles['categ_currency'], float_round(ctotal, precision_rounding=prec_cur_rounding)) else: - i -= 1 # re-write on previous categ - for x in range(cols['categ_subtotal']['pos']): - sheet.write(crow, x, '', regular) + i -= 1 # go back to skipped line # Write total total_formula = '=SUM(%s%d:%s%d)' % (letter_subtotal, total_row + 2, letter_subtotal, i + 1) - sheet.write_formula(total_row, cols['subtotal']['pos'], total_formula, total_currency, float_round(total, precision_rounding=prec_cur_rounding)) + sheet.write_formula(total_row, cols['subtotal']['pos'], total_formula, styles['total_currency'], float_round(total, precision_rounding=prec_cur_rounding)) workbook.close() logger.debug('End create XLSX workbook') @@ -468,3 +437,59 @@ class StockValuationXlsx(models.TransientModel): 'res_id': self.id, }) return action + + def _prepare_styles(self, workbook, company, prec_price): + total_bg_color = '#faa03a' + categ_bg_color = '#e1daf5' + col_title_bg_color = '#fff9b4' + regular_font_size = 10 + currency_num_format = u'# ### ##0.00 %s' % company.currency_id.symbol + price_currency_num_format = u'# ### ##0.%s %s' % ('0' * prec_price, company.currency_id.symbol) + styles = { + 'doc_title': workbook.add_format({ + 'bold': True, 'font_size': regular_font_size + 10, + 'font_color': '#003b6f'}), + 'doc_subtitle': workbook.add_format({ + 'bold': True, 'font_size': regular_font_size}), + 'col_title': workbook.add_format({ + 'bold': True, 'bg_color': col_title_bg_color, + 'text_wrap': True, 'font_size': regular_font_size, + 'align': 'center', + }), + 'total_title': workbook.add_format({ + 'bold': True, 'text_wrap': True, 'font_size': regular_font_size + 2, + 'align': 'right', 'bg_color': total_bg_color}), + 'total_currency': workbook.add_format({ + 'num_format': currency_num_format, 'bg_color': total_bg_color}), + 'regular_date': workbook.add_format({'num_format': 'dd/mm/yyyy'}), + 'regular_currency': workbook.add_format({'num_format': currency_num_format}), + 'regular_price_currency': workbook.add_format({'num_format': price_currency_num_format}), + 'regular': workbook.add_format({}), + 'regular_small': workbook.add_format({'font_size': regular_font_size - 2}), + 'categ_title': workbook.add_format({ + 'bold': True, 'bg_color': categ_bg_color, + 'font_size': regular_font_size}), + 'categ_currency': workbook.add_format({ + 'num_format': currency_num_format, 'bg_color': categ_bg_color}), + 'date_title': workbook.add_format({ + 'bold': True, 'font_size': regular_font_size, 'align': 'right'}), + 'date_title_val': workbook.add_format({ + 'bold': True, 'font_size': regular_font_size}), + } + return styles + + def _prepare_cols(self): + cols = { + 'default_code': {'width': 18, 'style': 'regular', 'sequence': 10, 'title': _('Product Code')}, + 'product_name': {'width': 40, 'style': 'regular', 'sequence': 20, 'title': _('Product Name')}, + 'loc_name': {'width': 25, 'style': 'regular_small', 'sequence': 30, 'title': _('Location Name')}, + 'lot_name': {'width': 18, 'style': 'regular', 'sequence': 40, 'title': _('Lot')}, + 'expiry_date': {'width': 11, 'style': 'regular_date', 'sequence': 50, 'title': _('Expiry Date'), 'type': 'date'}, + 'qty': {'width': 8, 'style': 'regular', 'sequence': 60, 'title': _('Qty')}, + 'uom_name': {'width': 5, 'style': 'regular_small', 'sequence': 70, 'title': _('UoM')}, + 'standard_price': {'width': 12, 'style': 'regular_price_currency', 'sequence': 80, 'title': _('Price')}, + 'subtotal': {'width': 16, 'style': 'regular_currency', 'sequence': 90, 'title': _('Sub-total'), 'formula': True}, + 'categ_subtotal': {'width': 16, 'style': 'regular_currency', 'sequence': 100, 'title': _('Categ Sub-total'), 'formula': True}, + 'categ_name': {'width': 40, 'style': 'regular_small', 'sequence': 110, 'title': _('Product Category')}, + } + return cols diff --git a/stock_valuation_xlsx/wizard/stock_valuation_xlsx_view.xml b/stock_valuation_xlsx/wizard/stock_valuation_xlsx_view.xml index 7f456e1..2f566e9 100644 --- a/stock_valuation_xlsx/wizard/stock_valuation_xlsx_view.xml +++ b/stock_valuation_xlsx/wizard/stock_valuation_xlsx_view.xml @@ -39,7 +39,7 @@