From 13e68ac0f5df314fa6a4492890cae3e1837dc23c Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Thu, 4 Nov 2021 12:37:34 +0100 Subject: [PATCH] Remove modules pos_no_product_template_menu and sale_purchase_no_product_template_menu We won't port those modules to v14 --- pos_no_product_template_menu/__init__.py | 0 pos_no_product_template_menu/__manifest__.py | 28 ---- pos_no_product_template_menu/pos_view.xml | 16 -- .../__init__.py | 0 .../__manifest__.py | 29 ---- .../i18n/fr.po | 33 ---- ...sale_purchase_no_product_template_menu.pot | 33 ---- .../view.xml | 63 -------- stock_valuation_xlsx/__init__.py | 1 + stock_valuation_xlsx/__manifest__.py | 7 +- .../views/stock_inventory.xml | 2 +- stock_valuation_xlsx/wizard/__init__.py | 1 + .../wizard/stock_valuation_xlsx.py | 141 ++++++++++++++---- .../wizard/stock_valuation_xlsx_view.xml | 3 + 14 files changed, 127 insertions(+), 230 deletions(-) delete mode 100644 pos_no_product_template_menu/__init__.py delete mode 100644 pos_no_product_template_menu/__manifest__.py delete mode 100644 pos_no_product_template_menu/pos_view.xml delete mode 100644 sale_purchase_no_product_template_menu/__init__.py delete mode 100644 sale_purchase_no_product_template_menu/__manifest__.py delete mode 100644 sale_purchase_no_product_template_menu/i18n/fr.po delete mode 100644 sale_purchase_no_product_template_menu/i18n/sale_purchase_no_product_template_menu.pot delete mode 100644 sale_purchase_no_product_template_menu/view.xml diff --git a/pos_no_product_template_menu/__init__.py b/pos_no_product_template_menu/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/pos_no_product_template_menu/__manifest__.py b/pos_no_product_template_menu/__manifest__.py deleted file mode 100644 index 3436e09..0000000 --- a/pos_no_product_template_menu/__manifest__.py +++ /dev/null @@ -1,28 +0,0 @@ -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -{ - 'name': 'POS No Product Template Menu', - 'version': '12.0.1.0.0', - 'category': 'Point of sale', - 'license': 'AGPL-3', - 'summary': "Replace product.template menu entries by product.product menu", - 'description': """ -POS No Product Template -======================= - -This module replaces the menu entry for product.template by menu entries -for product.product in the *Point Of Sale > Product* menu. - -This module also switches to the tree view by default -for Product menu entries, instead of the kanban view. - -This module has been written by David Béal -from Akretion . - """, - 'author': 'Akretion', - 'website': 'http://www.akretion.com', - 'depends': ['point_of_sale', 'sale_purchase_no_product_template_menu'], - 'auto_install': True, - 'data': ['pos_view.xml'], - 'installable': False, -} diff --git a/pos_no_product_template_menu/pos_view.xml b/pos_no_product_template_menu/pos_view.xml deleted file mode 100644 index 89af7e5..0000000 --- a/pos_no_product_template_menu/pos_view.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - Products - product.product - kanban,tree,form - {'default_available_in_pos': True, 'search_default_filter_to_availabe_pos': 1} - - - - - - - diff --git a/sale_purchase_no_product_template_menu/__init__.py b/sale_purchase_no_product_template_menu/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/sale_purchase_no_product_template_menu/__manifest__.py b/sale_purchase_no_product_template_menu/__manifest__.py deleted file mode 100644 index 7924249..0000000 --- a/sale_purchase_no_product_template_menu/__manifest__.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright 2015-2019 Akretion France (http://www.akretion.com/) -# @author: Alexis de Lattre -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -{ - 'name': 'Sale Purchase No Product Template Menu', - 'version': '12.0.1.0.0', - 'category': 'Sale and Purchase', - 'license': 'AGPL-3', - 'summary': "Replace product.template menu entries by product.product menu entries", - 'description': """ -Sale Purchase No Product Template -================================= - -This module replaces the menu entries for product.template by menu entries for product.product in the *Sales* and *Purchases* menu entries. With this module, the only menu entry for product.template is in the menu *Sales > Configuration > Product Categories and Attributes*. - -This module also switches to the tree view by default for Product menu entries, instead of the kanban view. - -This module has been written by Alexis de Lattre from Akretion . - """, - 'author': 'Akretion', - 'website': 'http://www.akretion.com', - 'depends': [ - 'purchase', - 'sale', - ], - 'data': ['view.xml'], - 'installable': False, -} diff --git a/sale_purchase_no_product_template_menu/i18n/fr.po b/sale_purchase_no_product_template_menu/i18n/fr.po deleted file mode 100644 index 9e549dd..0000000 --- a/sale_purchase_no_product_template_menu/i18n/fr.po +++ /dev/null @@ -1,33 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * sale_purchase_no_product_template_menu -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 8.0\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-05-30 15:27+0000\n" -"PO-Revision-Date: 2016-05-30 15:27+0000\n" -"Last-Translator: <>\n" -"Language-Team: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Plural-Forms: \n" - -#. module: sale_purchase_no_product_template_menu -#: model:ir.ui.menu,name:sale_purchase_no_product_template_menu.sale_config_product_template_menu -msgid "Product Templates" -msgstr "Modèles d'article" - -#. module: sale_purchase_no_product_template_menu -#: model:ir.actions.act_window,name:sale_purchase_no_product_template_menu.product_product_action_puchased -#: model:ir.actions.act_window,name:sale_purchase_no_product_template_menu.product_product_action_sell -msgid "Products" -msgstr "Articles" - -#. module: sale_purchase_no_product_template_menu -#: view:product.product:sale_purchase_no_product_template_menu.product_normal_form_view -msgid "{'invisible': 1, 'required': 0}" -msgstr "{'invisible': 1, 'required': 0}" - diff --git a/sale_purchase_no_product_template_menu/i18n/sale_purchase_no_product_template_menu.pot b/sale_purchase_no_product_template_menu/i18n/sale_purchase_no_product_template_menu.pot deleted file mode 100644 index 2d5822a..0000000 --- a/sale_purchase_no_product_template_menu/i18n/sale_purchase_no_product_template_menu.pot +++ /dev/null @@ -1,33 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * sale_purchase_no_product_template_menu -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 8.0\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-05-30 15:27+0000\n" -"PO-Revision-Date: 2016-05-30 15:27+0000\n" -"Last-Translator: <>\n" -"Language-Team: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Plural-Forms: \n" - -#. module: sale_purchase_no_product_template_menu -#: model:ir.ui.menu,name:sale_purchase_no_product_template_menu.sale_config_product_template_menu -msgid "Product Templates" -msgstr "" - -#. module: sale_purchase_no_product_template_menu -#: model:ir.actions.act_window,name:sale_purchase_no_product_template_menu.product_product_action_puchased -#: model:ir.actions.act_window,name:sale_purchase_no_product_template_menu.product_product_action_sell -msgid "Products" -msgstr "" - -#. module: sale_purchase_no_product_template_menu -#: view:product.product:sale_purchase_no_product_template_menu.product_normal_form_view -msgid "{'invisible': 1, 'required': 0}" -msgstr "" - diff --git a/sale_purchase_no_product_template_menu/view.xml b/sale_purchase_no_product_template_menu/view.xml deleted file mode 100644 index 0bf6ca1..0000000 --- a/sale_purchase_no_product_template_menu/view.xml +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - - - Products - product.product - tree,form,kanban - {'search_default_filter_to_purchase': 1} - - - - - - - - - - - - Products - product.product - tree,form,kanban - {'search_default_filter_to_sell': 1} - - - - - - - - - - - - - Product Templates - tree,form,kanban - - {} - - - - - - - - - tree,form,kanban - - - - diff --git a/stock_valuation_xlsx/__init__.py b/stock_valuation_xlsx/__init__.py index 4027237..9b42961 100644 --- a/stock_valuation_xlsx/__init__.py +++ b/stock_valuation_xlsx/__init__.py @@ -1 +1,2 @@ +from . import models from . import wizard diff --git a/stock_valuation_xlsx/__manifest__.py b/stock_valuation_xlsx/__manifest__.py index 1593277..f2eb764 100644 --- a/stock_valuation_xlsx/__manifest__.py +++ b/stock_valuation_xlsx/__manifest__.py @@ -5,7 +5,7 @@ { 'name': 'Stock Valuation XLSX', - 'version': '12.0.1.0.0', + 'version': '14.0.1.0.0', 'category': 'Tools', 'license': 'AGPL-3', 'summary': 'Generate XLSX reports for past or present stock levels', @@ -37,8 +37,11 @@ This module has been written by Alexis de Lattre from Akretion diff --git a/stock_valuation_xlsx/wizard/__init__.py b/stock_valuation_xlsx/wizard/__init__.py index 768a578..36fc960 100644 --- a/stock_valuation_xlsx/wizard/__init__.py +++ b/stock_valuation_xlsx/wizard/__init__.py @@ -1 +1,2 @@ from . import stock_valuation_xlsx +from . import stock_variation_xlsx diff --git a/stock_valuation_xlsx/wizard/stock_valuation_xlsx.py b/stock_valuation_xlsx/wizard/stock_valuation_xlsx.py index 99c933b..e048460 100644 --- a/stock_valuation_xlsx/wizard/stock_valuation_xlsx.py +++ b/stock_valuation_xlsx/wizard/stock_valuation_xlsx.py @@ -4,6 +4,7 @@ from odoo import models, fields, api, _ from odoo.exceptions import UserError +from dateutil.relativedelta import relativedelta from odoo.tools import float_is_zero, float_round from io import BytesIO from datetime import datetime @@ -18,7 +19,7 @@ class StockValuationXlsx(models.TransientModel): _name = 'stock.valuation.xlsx' _description = 'Generate XLSX report for stock valuation' - export_file = fields.Binary(string='XLSX Report', readonly=True) + export_file = fields.Binary(string='XLSX Report', readonly=True, attachment=True) export_filename = fields.Char(readonly=True) # I don't use ir.actions.url on v12, because it renders # the wizard unusable after the first report generation, which creates @@ -38,8 +39,10 @@ class StockValuationXlsx(models.TransientModel): help="The childen locations of the selected locations will " u"be taken in the valuation.") categ_ids = fields.Many2many( - 'product.category', string='Product Categories', - states={'done': [('readonly', True)]}) + 'product.category', string='Product Category Filter', + help="Leave this field empty to have a stock valuation for all your products.", + states={'done': [('readonly', True)]}, + ) source = fields.Selection([ ('inventory', 'Physical Inventory'), ('stock', 'Stock Levels'), @@ -59,17 +62,33 @@ class StockValuationXlsx(models.TransientModel): categ_subtotal = fields.Boolean( string='Subtotals per Categories', default=True, states={'done': [('readonly', True)]}, - help="Show a subtotal per product category") + help="Show a subtotal per product category.") standard_price_date = fields.Selection([ ('past', 'Past Date or Inventory Date'), ('present', 'Current'), ], default='past', string='Cost Price Date', states={'done': [('readonly', True)]}) + # I can't put a compute field for has_expiry_date + # because I want to have the value when the wizard is started, + # and not wait until run + has_expiry_date = fields.Boolean( + default=lambda self: self._default_has_expiry_date(), readonly=True) + apply_depreciation = fields.Boolean( + string='Apply Depreciation Rules', default=True, + states={'done': [('readonly', True)]}) split_by_lot = fields.Boolean( string='Display Lots', states={'done': [('readonly', True)]}) split_by_location = fields.Boolean( string='Display Stock Locations', states={'done': [('readonly', True)]}) + @api.model + def _default_has_expiry_date(self): + splo = self.env['stock.production.lot'] + has_expiry_date = False + if hasattr(splo, 'expiry_date'): + has_expiry_date = True + return has_expiry_date + @api.model def _default_location(self): wh = self.env.ref('stock.warehouse0') @@ -123,6 +142,17 @@ class StockValuationXlsx(models.TransientModel): def _prepare_product_fields(self): return ['uom_id', 'name', 'default_code', 'categ_id'] + def _prepare_expiry_depreciation_rules(self, company_id, past_date): + rules = self.env['stock.expiry.depreciation.rule'].search_read([('company_id', '=', company_id)], ['start_limit_days', 'ratio'], order='start_limit_days desc') + if past_date: + date_dt = past_date + else: + date_dt = fields.Date.context_today(self) + for rule in rules: + rule['start_date'] = date_dt - relativedelta(days=rule['start_limit_days']) + logger.debug('depreciation_rules=%s', rules) + return rules + def compute_product_data( self, company_id, in_stock_product_ids, standard_price_past_date=False): self.ensure_one() @@ -156,38 +186,56 @@ class StockValuationXlsx(models.TransientModel): logger.debug('End compute_product_data') return product_id2data - def id2name(self, product_ids): - logger.debug('Start id2name') + @api.model + def product_categ_id2name(self, categories): pco = self.env['product.category'] - splo = self.env['stock.production.lot'] - slo = self.env['stock.location'].with_context(active_test=False) - puo = self.env['uom.uom'].with_context(active_test=False) categ_id2name = {} categ_domain = [] - if self.categ_ids: - categ_domain = [('id', 'child_of', self.categ_ids.ids)] + if categories: + categ_domain = [('id', 'child_of', categories.ids)] for categ in pco.search_read(categ_domain, ['display_name']): categ_id2name[categ['id']] = categ['display_name'] + return categ_id2name + + @api.model + def uom_id2name(self): + puo = self.env['uom.uom'].with_context(active_test=False) uom_id2name = {} uoms = puo.search_read([], ['name']) for uom in uoms: uom_id2name[uom['id']] = uom['name'] + return uom_id2name + + @api.model + def prodlot_id2data(self, product_ids, has_expiry_date, depreciation_rules): + splo = self.env['stock.production.lot'] lot_id2data = {} lot_fields = ['name'] - if hasattr(splo, 'expiry_date'): + if has_expiry_date: lot_fields.append('expiry_date') lots = splo.search_read( [('product_id', 'in', product_ids)], lot_fields) for lot in lots: lot_id2data[lot['id']] = lot + lot_id2data[lot['id']]['depreciation_ratio'] = 0 + if depreciation_rules and lot.get('expiry_date'): + expiry_date = lot['expiry_date'] + for rule in depreciation_rules: + if expiry_date <= rule['start_date']: + lot_id2data[lot['id']]['depreciation_ratio'] = rule['ratio'] / 100.0 + break + return lot_id2data + + @api.model + def stock_location_id2name(self, location): + slo = self.env['stock.location'].with_context(active_test=False) loc_id2name = {} 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') - return categ_id2name, uom_id2name, lot_id2data, loc_id2name + return loc_id2name def compute_data_from_inventory(self, product_ids, prec_qty): self.ensure_one() @@ -275,7 +323,7 @@ class StockValuationXlsx(models.TransientModel): def stringify_and_sort_result( self, product_ids, product_id2data, data, prec_qty, prec_price, prec_cur_rounding, categ_id2name, - uom_id2name, lot_id2data, loc_id2name): + uom_id2name, lot_id2data, loc_id2name, apply_depreciation): logger.debug('Start stringify_and_sort_result') res = [] for l in data: @@ -284,17 +332,27 @@ class StockValuationXlsx(models.TransientModel): standard_price = float_round( product_id2data[product_id]['standard_price'], precision_digits=prec_price) - subtotal = float_round( + subtotal_before_depreciation = float_round( standard_price * qty, precision_rounding=prec_cur_rounding) + depreciation_ratio = 0 + if apply_depreciation and l['lot_id']: + depreciation_ratio = lot_id2data[l['lot_id']].get('depreciation_ratio', 0) + subtotal = float_round( + subtotal_before_depreciation * (1 - depreciation_ratio), + precision_rounding=prec_cur_rounding) + else: + subtotal = subtotal_before_depreciation 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'), + depreciation_ratio=depreciation_ratio, qty=qty, uom_name=uom_id2name[product_id2data[product_id]['uom_id']], standard_price=standard_price, + subtotal_before_depreciation=subtotal_before_depreciation, subtotal=subtotal, categ_name=categ_id2name[product_id2data[product_id]['categ_id']], )) @@ -313,6 +371,12 @@ class StockValuationXlsx(models.TransientModel): prec_cur_rounding = company.currency_id.rounding self._check_config(company_id) + apply_depreciation = self.apply_depreciation + if ( + (self.source == 'stock' and self.stock_date_type == 'past') or + not self.split_by_lot or + not self.has_expiry_date): + apply_depreciation = False product_ids = self.get_product_ids() if not product_ids: raise UserError(_("There are no products to analyse.")) @@ -335,15 +399,25 @@ class StockValuationXlsx(models.TransientModel): standard_price_past_date = past_date if not (self.source == 'stock' and self.stock_date_type == 'present') and self.standard_price_date == 'present': standard_price_past_date = False + depreciation_rules = [] + if apply_depreciation: + depreciation_rules = self._prepare_expiry_depreciation_rules(company_id, past_date) + if not depreciation_rules: + raise UserError(_( + "The are not stock depreciation rule for company '%s'.") + % company.display_name) in_stock_product_ids = list(in_stock_products.keys()) product_id2data = self.compute_product_data( company_id, in_stock_product_ids, standard_price_past_date=standard_price_past_date) data_res = self.group_result(data, split_by_lot, split_by_location) - categ_id2name, uom_id2name, lot_id2data, loc_id2name = self.id2name(product_ids) + categ_id2name = self.product_categ_id2name(self.categ_ids) + uom_id2name = self.uom_id2name() + lot_id2data = self.prodlot_id2data(in_stock_product_ids, self.has_expiry_date, depreciation_rules) + loc_id2name = self.stock_location_id2name(self.location_id) res = self.stringify_and_sort_result( product_ids, product_id2data, data_res, prec_qty, prec_price, prec_cur_rounding, - categ_id2name, uom_id2name, lot_id2data, loc_id2name) + categ_id2name, uom_id2name, lot_id2data, loc_id2name, apply_depreciation) logger.debug('Start create XLSX workbook') file_data = BytesIO() @@ -356,12 +430,15 @@ class StockValuationXlsx(models.TransientModel): if not split_by_lot: cols.pop('lot_name', None) cols.pop('expiry_date', None) - if not hasattr(splo, 'expiry_date'): + if not self.has_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) + if not apply_depreciation: + cols.pop('depreciation_ratio', None) + cols.pop('subtotal_before_depreciation', None) j = 0 for col, col_vals in sorted(cols.items(), key=lambda x: x[1]['sequence']): @@ -417,6 +494,9 @@ class StockValuationXlsx(models.TransientModel): letter_qty = cols['qty']['pos_letter'] letter_price = cols['standard_price']['pos_letter'] letter_subtotal = cols['subtotal']['pos_letter'] + if apply_depreciation: + letter_subtotal_before_depreciation = cols['subtotal_before_depreciation']['pos_letter'] + letter_depreciation_ratio = cols['depreciation_ratio']['pos_letter'] crow = 0 lines = res for categ_id in categ_ids: @@ -432,12 +512,20 @@ class StockValuationXlsx(models.TransientModel): total += l['subtotal'] ctotal += l['subtotal'] categ_has_line = True - subtotal_formula = '=%s%d*%s%d' % (letter_qty, i + 1, letter_price, i + 1) + qty_by_price_formula = '=%s%d*%s%d' % (letter_qty, i + 1, letter_price, i + 1) + if apply_depreciation: + sheet.write_formula(i, cols['subtotal_before_depreciation']['pos'], qty_by_price_formula, styles['regular_currency'], l['subtotal_before_depreciation']) + subtotal_formula = '=%s%d*(1 - %s%d)' % (letter_subtotal_before_depreciation, i + 1, letter_depreciation_ratio, i + 1) + else: + subtotal_formula = qty_by_price_formula sheet.write_formula(i, cols['subtotal']['pos'], subtotal_formula, styles['regular_currency'], l['subtotal']) for col_name, col in cols.items(): if not col.get('formula'): - if col.get('type') == 'date' and l[col_name]: - l[col_name] = fields.Date.from_string(l[col_name]) + if col.get('type') == 'date': + if l[col_name]: + l[col_name] = fields.Date.from_string(l[col_name]) + else: + l[col_name] = '' # to avoid display of 31/12/1899 sheet.write(i, col['pos'], l[col_name], styles[col['style']]) if categ_subtotal: if categ_has_line: @@ -503,6 +591,7 @@ class StockValuationXlsx(models.TransientModel): '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_int_percent': workbook.add_format({'num_format': u'0.%'}), 'regular': workbook.add_format({}), 'regular_small': workbook.add_format({'font_size': regular_font_size - 2}), 'categ_title': workbook.add_format({ @@ -527,8 +616,10 @@ class StockValuationXlsx(models.TransientModel): 'qty': {'width': 8, 'style': 'regular', 'sequence': 60, 'title': _('Qty')}, 'uom_name': {'width': 5, 'style': 'regular_small', 'sequence': 70, 'title': _('UoM')}, 'standard_price': {'width': 14, 'style': 'regular_price_currency', 'sequence': 80, 'title': _('Cost 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')}, + 'subtotal_before_depreciation': {'width': 16, 'style': 'regular_currency', 'sequence': 90, 'title': _('Sub-total'), 'formula': True}, + 'depreciation_ratio': {'width': 10, 'style': 'regular_int_percent', 'sequence': 100, 'title': _('Depreciation')}, + 'subtotal': {'width': 16, 'style': 'regular_currency', 'sequence': 110, 'title': _('Sub-total'), 'formula': True}, + 'categ_subtotal': {'width': 16, 'style': 'regular_currency', 'sequence': 120, 'title': _('Categ Sub-total'), 'formula': True}, + 'categ_name': {'width': 40, 'style': 'regular_small', 'sequence': 130, '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 16a35fb..bf90648 100644 --- a/stock_valuation_xlsx/wizard/stock_valuation_xlsx_view.xml +++ b/stock_valuation_xlsx/wizard/stock_valuation_xlsx_view.xml @@ -27,8 +27,10 @@ + + @@ -55,6 +57,7 @@ Stock Valuation XLSX + 0