From d2b63236e39dc3114d7314727e21a06499adaa46 Mon Sep 17 00:00:00 2001 From: hparfr Date: Wed, 29 Apr 2020 23:00:08 +0200 Subject: [PATCH 01/46] Fix stripe installation issue due to wrong account_type on transfert account template --- account_usability/account.py | 19 +++++++++++++++++++ account_usability/readme/DESCRIPTION.rst | 1 + 2 files changed, 20 insertions(+) diff --git a/account_usability/account.py b/account_usability/account.py index cb322adc..3b275ea6 100644 --- a/account_usability/account.py +++ b/account_usability/account.py @@ -693,3 +693,22 @@ class ResConfigSettings(models.TransientModel): transfer_account_id = fields.Many2one( related='company_id.transfer_account_id', readonly=False) + + +class AccountChartTemplate(models.Model): + _inherit = "account.chart.template" + + @api.model + def _prepare_transfer_account_template(self): + """Change the type of default account in order to be + compliant with _check_account_type_on_bank_journal + Used at installation of payment modules like stripe + See https://github.com/akretion/odoo-usability/issues/115 + """ + vals = super()._prepare_transfer_account_template() + current_assets_type = self.env.ref( + 'account.data_account_type_liquidity', raise_if_not_found=False) + vals.update({ + 'user_type_id': current_assets_type and current_assets_type.id or False, + }) + return vals diff --git a/account_usability/readme/DESCRIPTION.rst b/account_usability/readme/DESCRIPTION.rst index 36bac9f4..4a992970 100644 --- a/account_usability/readme/DESCRIPTION.rst +++ b/account_usability/readme/DESCRIPTION.rst @@ -31,6 +31,7 @@ This modules adds the following functions: * don't attach PDF upon invoice report generation on supplier invoices/refunds * Add filter on debit and credit amount for Move Lines * Add supplier invoice number in invoice tree view +* Change type from current_assets to liquidity for transfert account template. Together with this module, I recommend the use of the following modules: From 614c1a7ac211304bc2041edc7d6f51f1172b1e60 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Wed, 2 Dec 2020 20:59:41 +0100 Subject: [PATCH 02/46] Remove dead file --- purchase_usability/stock_view.xml | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100644 purchase_usability/stock_view.xml diff --git a/purchase_usability/stock_view.xml b/purchase_usability/stock_view.xml deleted file mode 100644 index a6d6c201..00000000 --- a/purchase_usability/stock_view.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - purchase_usability.stock.picking.form - stock.picking - - - - - - - - - - - From 2a3ca2d35c4d34ea06dc63d3c36fad1c9d2274d5 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Fri, 4 Dec 2020 10:49:35 +0100 Subject: [PATCH 03/46] sale_usability: up-port warning on price update when qty is changed --- sale_usability/sale.py | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/sale_usability/sale.py b/sale_usability/sale.py index f94f8f2c..f5c5d18f 100644 --- a/sale_usability/sale.py +++ b/sale_usability/sale.py @@ -2,8 +2,9 @@ # @author Alexis de Lattre # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import models, fields, api -from odoo.tools import float_is_zero +from odoo import models, fields, api, _ +from odoo.tools import float_is_zero, float_compare +from odoo.tools.misc import formatLang class SaleOrder(models.Model): @@ -63,3 +64,35 @@ def py3o_lines_layout(self): # {'subtotal': 8932.23}, # ] return res + + +class SaleOrderLine(models.Model): + _inherit = 'sale.order.line' + + @api.onchange('product_uom', 'product_uom_qty') + def product_uom_change(self): + # When the user has manually set a custom price + # he is often upset when Odoo changes it when he changes the qty + # So we add a warning in which we recall the old price. + res = {} + old_price = self.price_unit + super().product_uom_change() + new_price = self.price_unit + prec = self.env['decimal.precision'].precision_get('Product Price') + if float_compare(old_price, new_price, precision_digits=prec): + pricelist = self.order_id.pricelist_id + res['warning'] = { + 'title': _('Price updated'), + 'message': _( + "Due to the update of the ordered quantity on line '%s', " + "the price has been updated according to pricelist '%s'.\n" + "Old price: %s\n" + "New price: %s") % ( + self.name, + pricelist.display_name, + formatLang( + self.env, old_price, currency_obj=pricelist.currency_id), + formatLang( + self.env, new_price, currency_obj=pricelist.currency_id)) + } + return res From 4076946f48a8939c4bef244edceff4cbb7da1820 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Fri, 4 Dec 2020 18:29:54 +0100 Subject: [PATCH 04/46] mrp_usability: add location_dest_id in the tree view of finished product on MO --- mrp_usability/views/mrp_views.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mrp_usability/views/mrp_views.xml b/mrp_usability/views/mrp_views.xml index ba5c5c1e..a47f4727 100644 --- a/mrp_usability/views/mrp_views.xml +++ b/mrp_usability/views/mrp_views.xml @@ -26,6 +26,11 @@ + + + + + From 05ac567c7a261ed6e786bf489bf7544e4ee262b5 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Mon, 11 Jan 2021 19:01:24 +0100 Subject: [PATCH 05/46] Show location_id in tree view of stock.move.line in raw materials tab --- mrp_usability/views/mrp_views.xml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/mrp_usability/views/mrp_views.xml b/mrp_usability/views/mrp_views.xml index a47f4727..3fcfb798 100644 --- a/mrp_usability/views/mrp_views.xml +++ b/mrp_usability/views/mrp_views.xml @@ -34,6 +34,21 @@ + + stock.move + + + + + + + 0 + 1 + stock.group_stock_multi_locations + + + + stock.move From aa804e2dfde0ac83017f006b5d1a8b58d6a4a87d Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Wed, 13 Jan 2021 15:19:29 +0100 Subject: [PATCH 06/46] Add track_visibility='onchange' on all importants fields of mrp.bom --- mrp_usability/models/mrp.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/mrp_usability/models/mrp.py b/mrp_usability/models/mrp.py index 935e2d88..e222b457 100644 --- a/mrp_usability/models/mrp.py +++ b/mrp_usability/models/mrp.py @@ -1,9 +1,9 @@ -# © 2015-2016 Akretion (http://www.akretion.com) +# Copyright 2015-2021 Akretion (http://www.akretion.com) # @author Alexis de Lattre # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import api, models +from odoo import api, fields, models class MrpProduction(models.Model): @@ -18,3 +18,17 @@ def get_stock_move_sold_out_report(self, move): if diff == 0.0: return "" return diff + + +class MrpBom(models.Model): + _inherit = 'mrp.bom' + + code = fields.Char(track_visibility='onchange') + type = fields.Selection(track_visibility='onchange') + product_tmpl_id = fields.Many2one(track_visibility='onchange') + product_id = fields.Many2one(track_visibility='onchange') + product_qty = fields.Float(track_visibility='onchange') + product_uom_id = fields.Many2one(track_visibility='onchange') + routing_id = fields.Many2one(track_visibility='onchange') + ready_to_produce = fields.Selection(track_visibility='onchange') + picking_type_id = fields.Many2one(track_visibility='onchange') From 2fe0ad51c738db2caf33f601eb9a05530cd72950 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Fri, 29 Jan 2021 21:54:23 +0100 Subject: [PATCH 07/46] mrp_usability: add source and dest loc on stock.move.line form view of finished product --- mrp_usability/views/mrp_views.xml | 11 ++++ sale_stock_usability/i18n/fr.po | 106 ------------------------------ 2 files changed, 11 insertions(+), 106 deletions(-) delete mode 100644 sale_stock_usability/i18n/fr.po diff --git a/mrp_usability/views/mrp_views.xml b/mrp_usability/views/mrp_views.xml index 3fcfb798..c48a0549 100644 --- a/mrp_usability/views/mrp_views.xml +++ b/mrp_usability/views/mrp_views.xml @@ -60,6 +60,17 @@ + + stock.move.line + + + + + + + + + mrp.bom diff --git a/sale_stock_usability/i18n/fr.po b/sale_stock_usability/i18n/fr.po deleted file mode 100644 index 34e2f73b..00000000 --- a/sale_stock_usability/i18n/fr.po +++ /dev/null @@ -1,106 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * sale_stock_usability -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 8.0\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-07-13 12:36+0000\n" -"PO-Revision-Date: 2016-07-13 14:37+0200\n" -"Last-Translator: <>\n" -"Language-Team: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: \n" -"Language: fr\n" -"X-Generator: Poedit 1.8.7.1\n" - -#. module: sale_stock_usability -#: view:sale.order:sale_stock_usability.view_order_form_inherit -msgid "Delivery Orders" -msgstr "Livraisons" - -#. module: sale_stock_usability -#: view:procurement.group:sale_stock_usability.procurement_group_form_view -#: field:procurement.group,picking_ids:0 -msgid "Pickings" -msgstr "Préparations" - -#. module: sale_stock_usability -#: model:ir.model,name:sale_stock_usability.model_procurement_group -msgid "Procurement Requisition" -msgstr "Demande d'approvisionnement" - -#. module: sale_stock_usability -#: view:procurement.group:sale_stock_usability.procurement_group_form_view -#: field:procurement.group,sale_ids:0 -msgid "Sale Orders" -msgstr "Sale Orders" - -#. module: sale_stock_usability -#: model:ir.model,name:sale_stock_usability.model_sale_order -msgid "Sales Order" -msgstr "Bon de commande" - -#. module: sale_stock_usability -#: model:ir.model,name:sale_stock_usability.model_sale_order_line -msgid "Sales Order Line" -msgstr "Ligne de commandes de vente" - -#. module: sale_stock_usability -#: selection:sale.order,picking_status:0 -msgid "Delivery Cancelled" -msgstr "Livraison annulée" - -#. module: sale_stock_usability -#: model_terms:ir.ui.view,arch_db:sale_stock_usability.view_sales_order_filter -#: selection:sale.order,picking_status:0 -msgid "Fully Delivered" -msgstr "Entierement Livré " - -#. module: sale_stock_usability -#: model:ir.model.fields,field_description:sale_stock_usability.field_sale_order__incoterm -msgid "Incoterms" -msgstr "Incoterms" - -#. module: sale_stock_usability -#: model:ir.model.fields,help:sale_stock_usability.field_sale_order__incoterm -msgid "International Commercial Terms are a series of predefined commercial terms used in international transactions." -msgstr "Les Incoterms sont une série de termes commerciaux prédéfinie utilisés dans les transactions internationales." - -#. module: sale_stock_usability -#: model_terms:ir.ui.view,arch_db:sale_stock_usability.view_sales_order_filter -msgid "Not Fully Delivered" -msgstr "Livraison à faire" - -#. module: sale_stock_usability -#: selection:sale.order,picking_status:0 -msgid "Nothing to Deliver" -msgstr "Rien à livrer" - -#. module: sale_stock_usability -#: selection:sale.order,picking_status:0 -msgid "Partially Delivered" -msgstr "Livré partielement" - -#. module: sale_stock_usability -#: model:ir.model.fields,field_description:sale_stock_usability.field_sale_order__picking_status -msgid "Picking Status" -msgstr "Status de BL" - -#. module: sale_stock_usability -#: model:ir.model,name:sale_stock_usability.model_sale_order -msgid "Sale Order" -msgstr "Bon de commande" - -#. module: sale_stock_usability -#: selection:sale.order,picking_status:0 -msgid "To Deliver" -msgstr "Prêt à livrer" - -#. module: sale_stock_usability -#: model:ir.model.fields,field_description:sale_stock_usability.field_sale_order__warehouse_id -msgid "Warehouse" -msgstr "Entrepôt" From 09b73d199425d2887b7c6320e69ea6cf6960eedc Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Fri, 29 Jan 2021 21:55:26 +0100 Subject: [PATCH 08/46] mrp_usability: remove readonly on location field --- mrp_usability/views/mrp_views.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mrp_usability/views/mrp_views.xml b/mrp_usability/views/mrp_views.xml index c48a0549..36551ad3 100644 --- a/mrp_usability/views/mrp_views.xml +++ b/mrp_usability/views/mrp_views.xml @@ -43,7 +43,7 @@ 0 - 1 + 0 stock.group_stock_multi_locations From 872801fd294dc33242aa015e795ea9a98b920240 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Fri, 26 Feb 2021 11:09:51 +0100 Subject: [PATCH 09/46] stock_valuation_xlsx: add stock.variation.xlsx report (Up-port from v10) Up-port from v10 other fixes and improvements up to 2021-02-26 --- stock_valuation_xlsx/__manifest__.py | 1 + stock_valuation_xlsx/wizard/__init__.py | 1 + .../wizard/stock_valuation_xlsx.py | 44 +- .../wizard/stock_valuation_xlsx_view.xml | 1 + .../wizard/stock_variation_xlsx.py | 459 ++++++++++++++++++ .../wizard/stock_variation_xlsx_view.xml | 61 +++ 6 files changed, 553 insertions(+), 14 deletions(-) create mode 100644 stock_valuation_xlsx/wizard/stock_variation_xlsx.py create mode 100644 stock_valuation_xlsx/wizard/stock_variation_xlsx_view.xml diff --git a/stock_valuation_xlsx/__manifest__.py b/stock_valuation_xlsx/__manifest__.py index 65ba4352..97c78473 100644 --- a/stock_valuation_xlsx/__manifest__.py +++ b/stock_valuation_xlsx/__manifest__.py @@ -38,6 +38,7 @@ 'depends': ['stock_account'], 'data': [ 'wizard/stock_valuation_xlsx_view.xml', + 'wizard/stock_variation_xlsx_view.xml', 'views/stock_inventory.xml', ], 'installable': True, diff --git a/stock_valuation_xlsx/wizard/__init__.py b/stock_valuation_xlsx/wizard/__init__.py index 768a578f..36fc9606 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 99c933b6..8d48ad79 100644 --- a/stock_valuation_xlsx/wizard/stock_valuation_xlsx.py +++ b/stock_valuation_xlsx/wizard/stock_valuation_xlsx.py @@ -18,7 +18,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 +38,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,7 +61,7 @@ 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'), @@ -156,22 +158,29 @@ def compute_product_data( 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_id2name(self, product_ids): + splo = self.env['stock.production.lot'] lot_id2data = {} lot_fields = ['name'] if hasattr(splo, 'expiry_date'): @@ -181,13 +190,17 @@ def id2name(self, product_ids): [('product_id', 'in', product_ids)], lot_fields) for lot in lots: lot_id2data[lot['id']] = lot + 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() @@ -340,7 +353,10 @@ def generate(self): 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_id2name(in_stock_product_ids) + 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) diff --git a/stock_valuation_xlsx/wizard/stock_valuation_xlsx_view.xml b/stock_valuation_xlsx/wizard/stock_valuation_xlsx_view.xml index 16a35fb1..3500d23c 100644 --- a/stock_valuation_xlsx/wizard/stock_valuation_xlsx_view.xml +++ b/stock_valuation_xlsx/wizard/stock_valuation_xlsx_view.xml @@ -55,6 +55,7 @@ Stock Valuation XLSX + 0 diff --git a/stock_valuation_xlsx/wizard/stock_variation_xlsx.py b/stock_valuation_xlsx/wizard/stock_variation_xlsx.py new file mode 100644 index 00000000..0215f04d --- /dev/null +++ b/stock_valuation_xlsx/wizard/stock_variation_xlsx.py @@ -0,0 +1,459 @@ +# Copyright 2020-2021 Akretion France (http://www.akretion.com/) +# @author Alexis de Lattre +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import models, fields, api, _ +from odoo.exceptions import UserError +from odoo.tools import float_is_zero, float_round +from io import BytesIO +import base64 +from datetime import datetime +import xlsxwriter +import logging +logger = logging.getLogger(__name__) + + +class StockVariationXlsx(models.TransientModel): + _name = 'stock.variation.xlsx' + _description = 'Generate XLSX report for stock valuation variation between 2 dates' + + export_file = fields.Binary(string='XLSX Report', readonly=True, attachment=True) + export_filename = fields.Char(readonly=True) + state = fields.Selection([ + ('setup', 'Setup'), + ('done', 'Done'), + ], string='State', default='setup', readonly=True) + warehouse_id = fields.Many2one( + 'stock.warehouse', string='Warehouse', + states={'done': [('readonly', True)]}) + location_id = fields.Many2one( + 'stock.location', string='Root Stock Location', required=True, + domain=[('usage', 'in', ('view', 'internal'))], + default=lambda self: self._default_location(), + states={'done': [('readonly', True)]}, + help="The childen locations of the selected locations will " + "be taken in the valuation.") + categ_ids = fields.Many2many( + 'product.category', string='Product Category Filter', + help="Leave this fields empty to have a stock valuation for all your products.", + states={'done': [('readonly', True)]}) + start_date = fields.Datetime( + string='Start Date', required=True, + states={'done': [('readonly', True)]}) + standard_price_start_date_type = fields.Selection([ + ('start', 'Start Date'), + ('present', 'Current'), + ], default='start', required=True, + string='Cost Price for Start Date', + states={'done': [('readonly', True)]}) + end_date_type = fields.Selection([ + ('present', 'Present'), + ('past', 'Past'), + ], string='End Date Type', default='present', required=True, + states={'done': [('readonly', True)]}) + end_date = fields.Datetime( + string='End Date', states={'done': [('readonly', True)]}, + default=fields.Datetime.now) + standard_price_end_date_type = fields.Selection([ + ('end', 'End Date'), + ('present', 'Current'), + ], default='end', string='Cost Price for End Date', required=True, + states={'done': [('readonly', True)]}) + categ_subtotal = fields.Boolean( + string='Subtotals per Categories', default=True, + states={'done': [('readonly', True)]}, + help="Show a subtotal per product category.") + + @api.model + def _default_location(self): + wh = self.env.ref('stock.warehouse0') + return wh.lot_stock_id + + @api.onchange('warehouse_id') + def warehouse_id_change(self): + if self.warehouse_id: + self.location_id = self.warehouse_id.view_location_id.id + + def _check_config(self, company_id): + self.ensure_one() + present = fields.Datetime.now() + if self.end_date_type == 'past': + if not self.end_date: + raise UserError(_("End Date is missing.")) + if self.end_date > present: + raise UserError(_("The end date must be in the past.")) + if self.end_date <= self.start_date: + raise UserError(_("The start date must be before the end date.")) + else: + if self.start_date >= present: + raise UserError(_("The start date must be in the past.")) + 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() + domain = [('type', '=', 'product')] + if self.categ_ids: + domain += [('categ_id', 'child_of', self.categ_ids.ids)] + return domain + + def get_product_ids(self): + self.ensure_one() + domain = self._prepare_product_domain() + # Should we also add inactive products ?? + products = self.env['product.product'].search(domain) + return products.ids + + def _prepare_product_fields(self): + return ['uom_id', 'name', 'default_code', 'categ_id'] + + def compute_product_data( + self, company_id, filter_product_ids, + standard_price_start_date=False, standard_price_end_date=False): + self.ensure_one() + logger.debug('Start compute_product_data') + ppo = self.env['product.product'] + ppho = self.env['product.price.history'] + fields_list = self._prepare_product_fields() + if not standard_price_start_date or not standard_price_end_date: + fields_list.append('standard_price') + products = ppo.search_read([('id', 'in', filter_product_ids)], fields_list) + product_id2data = {} + 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 + if standard_price_start_date: + history = ppho.search_read([ + ('company_id', '=', company_id), + ('product_id', '=', p['id']), + ('datetime', '<=', standard_price_start_date)], + ['cost'], order='datetime desc, id desc', limit=1) + start_standard_price = history and history[0]['cost'] or 0.0 + else: + start_standard_price = p['standard_price'] + if standard_price_end_date: + history = ppho.search_read([ + ('company_id', '=', company_id), + ('product_id', '=', p['id']), + ('datetime', '<=', standard_price_end_date)], + ['cost'], order='datetime desc, id desc', limit=1) + end_standard_price = history and history[0]['cost'] or 0.0 + else: + end_standard_price = p['standard_price'] + + product_id2data[p['id']] = { + 'start_standard_price': start_standard_price, + 'end_standard_price': end_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 + + def compute_data_from_stock(self, product_ids, prec_qty, start_date, end_date_type, end_date, company_id): + self.ensure_one() + logger.debug('Start compute_data_from_stock past_date=%s end_date_type=%s, end_date=%s', start_date, end_date_type, end_date) + ppo = self.env['product.product'] + smo = self.env['stock.move'] + sqo = self.env['stock.quant'] + ppo_loc = ppo.with_context(location=self.location_id.id, force_company=company_id) + # Inspired by odoo/addons/stock/models/product.py + # method _compute_quantities_dict() + domain_quant_loc, domain_move_in_loc, domain_move_out_loc = ppo_loc._get_domain_locations() + domain_quant = [('product_id', 'in', product_ids)] + domain_quant_loc + domain_move_in = [('product_id', 'in', product_ids), ('state', '=', 'done')] + domain_move_in_loc + domain_move_out = [('product_id', 'in', product_ids), ('state', '=', 'done')] + domain_move_out_loc + quants_res = dict((item['product_id'][0], item['quantity']) for item in sqo.read_group(domain_quant, ['product_id', 'quantity'], ['product_id'], orderby='id')) + domain_move_in_start_to_end = [('date', '>', start_date)] + domain_move_in + domain_move_out_start_to_end = [('date', '>', start_date)] + domain_move_out + if end_date_type == 'past': + + domain_move_in_end_to_present = [('date', '>', end_date)] + domain_move_in + domain_move_out_end_to_present = [('date', '>', end_date)] + domain_move_out + moves_in_res_end_to_present = dict((item['product_id'][0], item['product_qty']) for item in smo.read_group(domain_move_in_end_to_present, ['product_id', 'product_qty'], ['product_id'], orderby='id')) + moves_out_res_end_to_present = dict((item['product_id'][0], item['product_qty']) for item in smo.read_group(domain_move_out_end_to_present, ['product_id', 'product_qty'], ['product_id'], orderby='id')) + + domain_move_in_start_to_end += [('date', '<', end_date)] + domain_move_out_start_to_end += [('date', '<', end_date)] + + moves_in_res_start_to_end = dict((item['product_id'][0], item['product_qty']) for item in smo.read_group(domain_move_in_start_to_end, ['product_id', 'product_qty'], ['product_id'], orderby='id')) + moves_out_res_start_to_end = dict((item['product_id'][0], item['product_qty']) for item in smo.read_group(domain_move_out_start_to_end, ['product_id', 'product_qty'], ['product_id'], orderby='id')) + + product_data = {} # key = product_id , value = dict + for product in ppo.browse(product_ids): + end_qty = quants_res.get(product.id, 0.0) + if end_date_type == 'past': + end_qty += moves_out_res_end_to_present.get(product.id, 0.0) - moves_in_res_end_to_present.get(product.id, 0.0) + in_qty = moves_in_res_start_to_end.get(product.id, 0.0) + out_qty = moves_out_res_start_to_end.get(product.id, 0.0) + start_qty = end_qty - in_qty + out_qty + if ( + not float_is_zero(start_qty, precision_digits=prec_qty) or + not float_is_zero(in_qty, precision_digits=prec_qty) or + not float_is_zero(out_qty, precision_digits=prec_qty) or + not float_is_zero(end_qty, precision_digits=prec_qty)): + product_data[product.id] = { + 'product_id': product.id, + 'start_qty': start_qty, + 'in_qty': in_qty, + 'out_qty': out_qty, + 'end_qty': end_qty, + } + logger.debug('End compute_data_from_stock') + return product_data + + def stringify_and_sort_result( + self, product_data, product_id2data, prec_qty, prec_price, prec_cur_rounding, + categ_id2name, uom_id2name): + logger.debug('Start stringify_and_sort_result') + res = [] + for product_id, l in product_data.items(): + start_qty = float_round(l['start_qty'], precision_digits=prec_qty) + in_qty = float_round(l['in_qty'], precision_digits=prec_qty) + out_qty = float_round(l['out_qty'], precision_digits=prec_qty) + end_qty = float_round(l['end_qty'], precision_digits=prec_qty) + start_standard_price = float_round( + product_id2data[product_id]['start_standard_price'], + precision_digits=prec_price) + end_standard_price = float_round( + product_id2data[product_id]['end_standard_price'], + precision_digits=prec_price) + start_subtotal = float_round( + start_standard_price * start_qty, precision_rounding=prec_cur_rounding) + end_subtotal = float_round( + end_standard_price * end_qty, precision_rounding=prec_cur_rounding) + variation = float_round( + end_subtotal - start_subtotal, precision_rounding=prec_cur_rounding) + res.append(dict( + product_id2data[product_id], + product_name=product_id2data[product_id]['name'], + start_qty=start_qty, + start_standard_price=start_standard_price, + start_subtotal=start_subtotal, + in_qty=in_qty, + out_qty=out_qty, + end_qty=end_qty, + end_standard_price=end_standard_price, + end_subtotal=end_subtotal, + variation=variation, + uom_name=uom_id2name[product_id2data[product_id]['uom_id']], + 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 + + def generate(self): + self.ensure_one() + logger.debug('Start generate XLSX stock variation report') + svxo = self.env['stock.valuation.xlsx'] + 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) + + product_ids = self.get_product_ids() + if not product_ids: + raise UserError(_("There are no products to analyse.")) + + product_data = self.compute_data_from_stock( + product_ids, prec_qty, self.start_date, self.end_date_type, self.end_date, + company_id) + standard_price_start_date = standard_price_end_date = False + if self.standard_price_start_date_type == 'start': + standard_price_start_date = self.start_date + if self.standard_price_end_date_type == 'end': + standard_price_end_date = self.end_date + + product_id2data = self.compute_product_data( + company_id, list(product_data.keys()), + standard_price_start_date, standard_price_end_date) + categ_id2name = svxo.product_categ_id2name(self.categ_ids) + uom_id2name = svxo.uom_id2name() + res = self.stringify_and_sort_result( + product_data, product_id2data, prec_qty, prec_price, prec_cur_rounding, + categ_id2name, uom_id2name) + + logger.debug('Start create XLSX workbook') + file_data = BytesIO() + workbook = xlsxwriter.Workbook(file_data) + sheet = workbook.add_worksheet('Stock_Variation') + styles = svxo._prepare_styles(workbook, company, prec_price) + cols = self._prepare_cols() + categ_subtotal = self.categ_subtotal + # remove cols that we won't use + if not categ_subtotal: + cols.pop('categ_subtotal', None) + + j = 0 + 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 + now_dt = fields.Datetime.context_timestamp(self, datetime.now()) + now_str = fields.Datetime.to_string(now_dt) + start_time_utc_dt = self.start_date + start_time_dt = fields.Datetime.context_timestamp(self, start_time_utc_dt) + start_time_str = fields.Datetime.to_string(start_time_dt) + if self.end_date_type == 'past': + end_time_utc_dt = self.end_date + end_time_dt = fields.Datetime.context_timestamp(self, end_time_utc_dt) + end_time_str = fields.Datetime.to_string(end_time_dt) + else: + end_time_str = now_str + if standard_price_start_date: + standard_price_start_date_str = start_time_str + else: + standard_price_start_date_str = now_str + if standard_price_end_date: + standard_price_end_date_str = end_time_str + else: + standard_price_end_date_str = now_str + i = 0 + sheet.write(i, 0, 'Odoo - Stock Valuation Variation', styles['doc_title']) + sheet.set_row(0, 26) + i += 1 + sheet.write(i, 0, 'Start Date: %s' % start_time_str, styles['doc_subtitle']) + i += 1 + sheet.write(i, 0, 'Cost Price Start Date: %s' % standard_price_start_date_str, styles['doc_subtitle']) + i += 1 + sheet.write(i, 0, 'End Date: %s' % end_time_str, styles['doc_subtitle']) + i += 1 + sheet.write(i, 0, 'Cost Price End Date: %s' % standard_price_end_date_str, styles['doc_subtitle']) + i += 1 + 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]), styles['doc_subtitle']) + i += 1 + sheet.write(i, 0, 'Generated on %s by %s' % (now_str, self.env.user.name), styles['regular_small']) + + # TITLE of COLS + i += 2 + for col in cols.values(): + sheet.write(i, col['pos'], col['title'], styles['col_title']) + + i += 1 + sheet.write(i, 0, _("TOTALS:"), styles['total_title']) + total_row = i + + # LINES + if categ_subtotal: + categ_ids = categ_id2name.keys() + else: + categ_ids = [0] + + start_total = end_total = variation_total = 0.0 + letter_start_qty = cols['start_qty']['pos_letter'] + letter_in_qty = cols['in_qty']['pos_letter'] + letter_out_qty = cols['out_qty']['pos_letter'] + letter_end_qty = cols['end_qty']['pos_letter'] + letter_start_price = cols['start_standard_price']['pos_letter'] + letter_end_price = cols['end_standard_price']['pos_letter'] + letter_start_subtotal = cols['start_subtotal']['pos_letter'] + letter_end_subtotal = cols['end_subtotal']['pos_letter'] + letter_variation = cols['variation']['pos_letter'] + crow = 0 + lines = res + for categ_id in categ_ids: + ctotal = 0.0 + categ_has_line = False + if categ_subtotal: + # skip a line and save it's position as crow + i += 1 + crow = i + lines = filter(lambda x: x['categ_id'] == categ_id, res) + for l in lines: + i += 1 + start_total += l['start_subtotal'] + end_total += l['end_subtotal'] + variation_total += l['variation'] + ctotal += l['variation'] + categ_has_line = True + end_qty_formula = '=%s%d+%s%d-%s%d' % (letter_start_qty, i + 1, letter_in_qty, i + 1, letter_out_qty, i + 1) + sheet.write_formula(i, cols['end_qty']['pos'], end_qty_formula, styles[cols['end_qty']['style']], l['end_qty']) + start_subtotal_formula = '=%s%d*%s%d' % (letter_start_qty, i + 1, letter_start_price, i + 1) + sheet.write_formula(i, cols['start_subtotal']['pos'], start_subtotal_formula, styles[cols['start_subtotal']['style']], l['start_subtotal']) + end_subtotal_formula = '=%s%d*%s%d' % (letter_end_qty, i + 1, letter_end_price, i + 1) + sheet.write_formula(i, cols['end_subtotal']['pos'], end_subtotal_formula, styles[cols['end_subtotal']['style']], l['end_subtotal']) + variation_formula = '=%s%d-%s%d' % (letter_end_subtotal, i + 1, letter_start_subtotal, i + 1) + sheet.write_formula(i, cols['variation']['pos'], variation_formula, styles[cols['variation']['style']], l['variation']) + sheet.write_formula(i, cols['end_subtotal']['pos'], end_subtotal_formula, styles[cols['end_subtotal']['style']], l['end_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]) + 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_variation, crow + 2, letter_variation, i + 1) + sheet.write_formula(crow, cols['categ_subtotal']['pos'], cformula, styles['categ_currency'], float_round(ctotal, precision_rounding=prec_cur_rounding)) + else: + i -= 1 # go back to skipped line + + # Write total + start_total_formula = '=SUM(%s%d:%s%d)' % (letter_start_subtotal, total_row + 2, letter_start_subtotal, i + 1) + sheet.write_formula(total_row, cols['start_subtotal']['pos'], start_total_formula, styles['total_currency'], float_round(start_total, precision_rounding=prec_cur_rounding)) + end_total_formula = '=SUM(%s%d:%s%d)' % (letter_end_subtotal, total_row + 2, letter_end_subtotal, i + 1) + sheet.write_formula(total_row, cols['end_subtotal']['pos'], end_total_formula, styles['total_currency'], float_round(end_total, precision_rounding=prec_cur_rounding)) + variation_total_formula = '=SUM(%s%d:%s%d)' % (letter_variation, total_row + 2, letter_variation, i + 1) + sheet.write_formula(total_row, cols['variation']['pos'], variation_total_formula, styles['total_currency'], float_round(variation_total, precision_rounding=prec_cur_rounding)) + + workbook.close() + logger.debug('End create XLSX workbook') + file_data.seek(0) + filename = 'Odoo_stock_%s_%s.xlsx' % ( + start_time_str.replace(' ', '-').replace(':', '_'), + end_time_str.replace(' ', '-').replace(':', '_')) + export_file_b64 = base64.b64encode(file_data.read()) + self.write({ + 'state': 'done', + 'export_filename': filename, + 'export_file': export_file_b64, + }) + action = self.env['ir.actions.act_window'].for_xml_id( + 'stock_valuation_xlsx', 'stock_variation_xlsx_action') + action['res_id'] = self.id + return action + + 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')}, + 'uom_name': {'width': 5, 'style': 'regular_small', 'sequence': 30, 'title': _('UoM')}, + 'start_qty': {'width': 8, 'style': 'regular', 'sequence': 40, 'title': _('Start Qty')}, + 'start_standard_price': {'width': 14, 'style': 'regular_price_currency', 'sequence': 50, 'title': _('Start Cost Price')}, + 'start_subtotal': {'width': 16, 'style': 'regular_currency', 'sequence': 60, 'title': _('Start Value'), 'formula': True}, + 'in_qty': {'width': 8, 'style': 'regular', 'sequence': 70, 'title': _('In Qty')}, + 'out_qty': {'width': 8, 'style': 'regular', 'sequence': 80, 'title': _('Out Qty')}, + 'end_qty': {'width': 8, 'style': 'regular', 'sequence': 90, 'title': _('End Qty'), 'formula': True}, + 'end_standard_price': {'width': 14, 'style': 'regular_price_currency', 'sequence': 100, 'title': _('End Cost Price')}, + 'end_subtotal': {'width': 16, 'style': 'regular_currency', 'sequence': 110, 'title': _('End Value'), 'formula': True}, + 'variation': {'width': 16, 'style': 'regular_currency', 'sequence': 120, 'title': _('Variation'), 'formula': True}, + 'categ_subtotal': {'width': 16, 'style': 'regular_currency', 'sequence': 130, 'title': _('Categ Sub-total'), 'formula': True}, + 'categ_name': {'width': 40, 'style': 'regular_small', 'sequence': 140, 'title': _('Product Category')}, + } + return cols diff --git a/stock_valuation_xlsx/wizard/stock_variation_xlsx_view.xml b/stock_valuation_xlsx/wizard/stock_variation_xlsx_view.xml new file mode 100644 index 00000000..9e3ddc77 --- /dev/null +++ b/stock_valuation_xlsx/wizard/stock_variation_xlsx_view.xml @@ -0,0 +1,61 @@ + + + + + + + + stock.variation.xlsx.form + stock.variation.xlsx + +
+
+

The generated XLSX report has the valuation of stockable products located on the selected stock locations (and their childrens).

+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ + + Stock Variation XLSX + stock.variation.xlsx + form + new + + + + + +
From 93272fb571e030176100c15aa3d90600ae14c29a Mon Sep 17 00:00:00 2001 From: David Beal Date: Tue, 9 Mar 2021 10:50:54 +0100 Subject: [PATCH 10/46] Update menu_view.xml --- developer_menu/menu_view.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/developer_menu/menu_view.xml b/developer_menu/menu_view.xml index 7f47679c..6cfddb2d 100644 --- a/developer_menu/menu_view.xml +++ b/developer_menu/menu_view.xml @@ -2,7 +2,7 @@ - + From 97fe8b463ddd78c4885e76ca0494e8ce1e4aa264 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Sat, 13 Mar 2021 00:24:04 +0100 Subject: [PATCH 11/46] mrp_usability: add tracking on date fields --- mrp_usability/models/mrp.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mrp_usability/models/mrp.py b/mrp_usability/models/mrp.py index e222b457..6de4202c 100644 --- a/mrp_usability/models/mrp.py +++ b/mrp_usability/models/mrp.py @@ -10,6 +10,9 @@ class MrpProduction(models.Model): _inherit = 'mrp.production' _order = 'id desc' + date_planned_start = fields.Datetime(track_visibility='onchange') + date_planned_finished = fields.Datetime(track_visibility='onchange') + @api.model def get_stock_move_sold_out_report(self, move): lines = move.active_move_line_ids From 9b8966154582e9932037804b94b4b8ac03dcc915 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Mon, 22 Mar 2021 12:10:34 +0100 Subject: [PATCH 12/46] Fix crash in sale_down_payment --- sale_down_payment/models/account_payment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sale_down_payment/models/account_payment.py b/sale_down_payment/models/account_payment.py index a10af15a..c7893899 100644 --- a/sale_down_payment/models/account_payment.py +++ b/sale_down_payment/models/account_payment.py @@ -49,7 +49,7 @@ def default_get(self, fields_list): def _compute_payment_amount(self, invoices=None, currency=None): amount = super(AccountAbstractPayment, self)._compute_payment_amount( invoices=invoices, currency=currency) - if self.sale_id: + if hasattr(self, 'sale_id') and self.sale_id: payment_currency = currency if not payment_currency: payment_currency = self.sale_id.currency_id From 592a82a41760a3cfd703f7f618b933b992235286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Valyi?= Date: Wed, 12 May 2021 15:16:19 -0300 Subject: [PATCH 13/46] fix message_post v12 API in unreserve (#145) * fix message_post v12 API in unreserve * import missing UserError --- stock_usability/stock.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/stock_usability/stock.py b/stock_usability/stock.py index c71c38f6..c0c67dd0 100644 --- a/stock_usability/stock.py +++ b/stock_usability/stock.py @@ -3,6 +3,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo import models, fields, api, _ +from odoo.exceptions import UserError import logging logger = logging.getLogger(__name__) @@ -98,7 +99,7 @@ def button_do_unreserve(self): picking = move.picking_id if picking: product = move.product_id - picking.message_post(_( + picking.message_post(body=_( "Product %s qty %s %s unreserved") % (product.id, product.display_name, @@ -120,7 +121,7 @@ def button_do_unreserve(self): picking = moveline.move_id.picking_id if picking: product = moveline.product_id - picking.message_post(_( + picking.message_post(body=_( "Product %s qty %s %s unreserved") % (product.id, product.display_name, From dbbd14f58ab58ec4a5eee2092e12b175f216b26a Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Thu, 27 May 2021 23:08:17 +0200 Subject: [PATCH 14/46] sale_down_payment: add a hook that can be used for notifications --- sale_down_payment/models/account_move_line.py | 5 +++++ sale_down_payment/models/account_payment.py | 8 ++++++++ sale_down_payment/wizard/account_bank_statement_sale.py | 4 +++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/sale_down_payment/models/account_move_line.py b/sale_down_payment/models/account_move_line.py index 601f3efe..347a12d1 100644 --- a/sale_down_payment/models/account_move_line.py +++ b/sale_down_payment/models/account_move_line.py @@ -30,3 +30,8 @@ def sale_id_check(self): def sale_advance_payement_account_id_change(self): if self.sale_id and self.account_id.user_type_id.type != 'receivable': self.sale_id = False + + def _sale_down_payment_hook(self): + # can be used for notifications + self.ensure_one() + diff --git a/sale_down_payment/models/account_payment.py b/sale_down_payment/models/account_payment.py index c7893899..0eaaf49f 100644 --- a/sale_down_payment/models/account_payment.py +++ b/sale_down_payment/models/account_payment.py @@ -25,6 +25,14 @@ def _get_counterpart_move_line_vals(self, invoice=False): res['sale_id'] = self.sale_id.id return res + def _create_payment_entry(self, amount): + move = super()._create_payment_entry(amount) + if hasattr(self, 'sale_id') and self.sale_id: + for line in move.line_ids: + if line.sale_id and line.account_id.internal_type == 'receivable': + line._sale_down_payment_hook() + return move + class AccountAbstractPayment(models.AbstractModel): _inherit = "account.abstract.payment" diff --git a/sale_down_payment/wizard/account_bank_statement_sale.py b/sale_down_payment/wizard/account_bank_statement_sale.py index 8d86a85c..5288dd7e 100644 --- a/sale_down_payment/wizard/account_bank_statement_sale.py +++ b/sale_down_payment/wizard/account_bank_statement_sale.py @@ -64,7 +64,9 @@ def validate(self): self.ensure_one() for line in self.line_ids: if line.move_line_id.sale_id != line.sale_id: - line.move_line_id.sale_id = line.sale_id.id + line.move_line_id.write({'sale_id': line.sale_id.id or False}) + if line.sale_id: + line.move_line_id._sale_down_payment_hook() class AccountBankStatementSaleLine(models.TransientModel): From 25a177eb760182cd04da5d554082f3aeb5a6aa2a Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Thu, 27 May 2021 23:58:03 +0200 Subject: [PATCH 15/46] Add module sale_mrp_usability (backport of a feature of v14) --- sale_mrp_usability/__init__.py | 1 + sale_mrp_usability/__manifest__.py | 23 ++++++++++++ sale_mrp_usability/models/__init__.py | 2 ++ sale_mrp_usability/models/mrp_production.py | 39 +++++++++++++++++++++ sale_mrp_usability/models/sale.py | 39 +++++++++++++++++++++ sale_mrp_usability/views/mrp_production.xml | 22 ++++++++++++ sale_mrp_usability/views/sale_order.xml | 23 ++++++++++++ stock_usability/procurement.py | 3 ++ 8 files changed, 152 insertions(+) create mode 100644 sale_mrp_usability/__init__.py create mode 100644 sale_mrp_usability/__manifest__.py create mode 100644 sale_mrp_usability/models/__init__.py create mode 100644 sale_mrp_usability/models/mrp_production.py create mode 100644 sale_mrp_usability/models/sale.py create mode 100644 sale_mrp_usability/views/mrp_production.xml create mode 100644 sale_mrp_usability/views/sale_order.xml diff --git a/sale_mrp_usability/__init__.py b/sale_mrp_usability/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/sale_mrp_usability/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/sale_mrp_usability/__manifest__.py b/sale_mrp_usability/__manifest__.py new file mode 100644 index 00000000..ad587760 --- /dev/null +++ b/sale_mrp_usability/__manifest__.py @@ -0,0 +1,23 @@ +# Copyright 2021 Akretion France (http://www.akretion.com) +# @author Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + 'name': 'Sale MRP Usability', + 'version': '12.0.1.0.0', + 'category': 'Sales', + 'license': 'AGPL-3', + 'summary': 'Usability improvements on sale_mrp module', + 'author': 'Akretion', + 'website': 'http://www.akretion.com', + 'depends': [ + 'sale_mrp', + 'stock_usability', + ], + 'data': [ + # Native in v14. Do no up-port to v14 + 'views/mrp_production.xml', + 'views/sale_order.xml', + ], + 'installable': True, +} diff --git a/sale_mrp_usability/models/__init__.py b/sale_mrp_usability/models/__init__.py new file mode 100644 index 00000000..c0df513c --- /dev/null +++ b/sale_mrp_usability/models/__init__.py @@ -0,0 +1,2 @@ +from . import sale +from . import mrp_production diff --git a/sale_mrp_usability/models/mrp_production.py b/sale_mrp_usability/models/mrp_production.py new file mode 100644 index 00000000..68a919c4 --- /dev/null +++ b/sale_mrp_usability/models/mrp_production.py @@ -0,0 +1,39 @@ +# Backport from Odoo v14 +# Copyright Odoo SA +# Same licence as Odoo (LGPL) + +from odoo import api, fields, models, _ + + +class MrpProduction(models.Model): + _inherit = 'mrp.production' + + sale_order_count = fields.Integer( + "Count of Source SO", + compute='_compute_sale_order_count', + groups='sales_team.group_sale_salesman') + + @api.depends('move_dest_ids.group_id.sale_id') + def _compute_sale_order_count(self): + for production in self: + production.sale_order_count = len(production.move_dest_ids.mapped('group_id').mapped('sale_id')) + + def action_view_sale_orders(self): + self.ensure_one() + sale_order_ids = self.move_dest_ids.mapped('group_id').mapped('sale_id').ids + action = { + 'res_model': 'sale.order', + 'type': 'ir.actions.act_window', + } + if len(sale_order_ids) == 1: + action.update({ + 'view_mode': 'form', + 'res_id': sale_order_ids[0], + }) + else: + action.update({ + 'name': _("Sources Sale Orders of %s" % self.name), + 'domain': [('id', 'in', sale_order_ids)], + 'view_mode': 'tree,form', + }) + return action diff --git a/sale_mrp_usability/models/sale.py b/sale_mrp_usability/models/sale.py new file mode 100644 index 00000000..92601e17 --- /dev/null +++ b/sale_mrp_usability/models/sale.py @@ -0,0 +1,39 @@ +# This code is a backport from odoo v14 +# Copyright Odoo SA +# Same licence as Odoo (LGPL) + +from odoo import api, fields, models, _ + + +class SaleOrder(models.Model): + _inherit = 'sale.order' + + mrp_production_count = fields.Integer( + "Count of MO generated", + compute='_compute_mrp_production_count', + groups='mrp.group_mrp_user') + + @api.depends('procurement_group_id.stock_move_ids.created_production_id') + def _compute_mrp_production_count(self): + for sale in self: + sale.mrp_production_count = len(sale.procurement_group_id.stock_move_ids.mapped('created_production_id')) + + def action_view_mrp_production(self): + self.ensure_one() + mrp_production_ids = self.procurement_group_id.stock_move_ids.mapped('created_production_id').ids + action = { + 'res_model': 'mrp.production', + 'type': 'ir.actions.act_window', + } + if len(mrp_production_ids) == 1: + action.update({ + 'view_mode': 'form', + 'res_id': mrp_production_ids[0], + }) + else: + action.update({ + 'name': _("Manufacturing Orders Generated by %s" % self.name), + 'domain': [('id', 'in', mrp_production_ids)], + 'view_mode': 'tree,form', + }) + return action diff --git a/sale_mrp_usability/views/mrp_production.xml b/sale_mrp_usability/views/mrp_production.xml new file mode 100644 index 00000000..fe861b29 --- /dev/null +++ b/sale_mrp_usability/views/mrp_production.xml @@ -0,0 +1,22 @@ + + + + + + mrp.production + + + + + + + + + diff --git a/sale_mrp_usability/views/sale_order.xml b/sale_mrp_usability/views/sale_order.xml new file mode 100644 index 00000000..03824eed --- /dev/null +++ b/sale_mrp_usability/views/sale_order.xml @@ -0,0 +1,23 @@ + + + + + + sale.order + + + + + + + + + diff --git a/stock_usability/procurement.py b/stock_usability/procurement.py index f0974377..f0adf90a 100644 --- a/stock_usability/procurement.py +++ b/stock_usability/procurement.py @@ -12,6 +12,9 @@ class ProcurementGroup(models.Model): _inherit = 'procurement.group' + # this field stock_move_ids is native in v14 + stock_move_ids = fields.One2many('stock.move', 'group_id', string="Related Stock Moves") + @api.model def _procure_orderpoint_confirm( self, use_new_cursor=False, company_id=False): From 8e01d32fe6929ae00c6395bcac9c9ab554f057e6 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Tue, 20 Jul 2021 12:23:41 +0200 Subject: [PATCH 16/46] Add tracking on some fields --- mrp_average_cost/models/mrp.py | 2 +- sale_usability/sale.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/mrp_average_cost/models/mrp.py b/mrp_average_cost/models/mrp.py index 8648c0f2..e81d633c 100644 --- a/mrp_average_cost/models/mrp.py +++ b/mrp_average_cost/models/mrp.py @@ -75,7 +75,7 @@ def _compute_total_cost(self): total_labour_cost = fields.Float( compute='_compute_total_labour_cost', readonly=True, digits=dp.get_precision('Product Price'), - string="Total Labour Cost", store=True) + string="Total Labour Cost", store=True, track_visibility='onchange') extra_cost = fields.Float( string='Extra Cost', track_visibility='onchange', digits=dp.get_precision('Product Price'), diff --git a/sale_usability/sale.py b/sale_usability/sale.py index f5c5d18f..668d088e 100644 --- a/sale_usability/sale.py +++ b/sale_usability/sale.py @@ -12,6 +12,7 @@ class SaleOrder(models.Model): date_order = fields.Datetime(track_visibility='onchange') confirmation_date = fields.Datetime(track_visibility='onchange') + commitment_date = fields.Datetime(track_visibility='onchange') client_order_ref = fields.Char(track_visibility='onchange') # for partner_id, the 'sale' module sets track_visibility='always' partner_id = fields.Many2one(track_visibility='onchange') From 9a67836c50ec31ab322559602713ed2b709c5811 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Tue, 20 Jul 2021 13:07:36 +0200 Subject: [PATCH 17/46] Improve stock inventory tree view --- stock_usability/stock_view.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/stock_usability/stock_view.xml b/stock_usability/stock_view.xml index 14430561..f408b3e4 100644 --- a/stock_usability/stock_view.xml +++ b/stock_usability/stock_view.xml @@ -361,6 +361,7 @@ should be able to access it. So I add a menu entry under Inventory Control. --> + state == 'draft' From 8da01d066519802ef9295c806721220f4542c903 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Mon, 13 Sep 2021 21:50:21 +0200 Subject: [PATCH 18/46] Add script to fix old rounding data bug --- account_usability/account.py | 54 +++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/account_usability/account.py b/account_usability/account.py index d798d342..b238213c 100644 --- a/account_usability/account.py +++ b/account_usability/account.py @@ -3,7 +3,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo import models, fields, api, _ -from odoo.tools import float_compare, float_is_zero +from odoo.tools import float_compare, float_is_zero, float_round from odoo.tools.misc import formatLang from odoo.exceptions import UserError, ValidationError from odoo.osv import expression @@ -407,6 +407,58 @@ def _compute_default_credit_debit(self): move.default_credit = default_credit move.default_debit = default_debit + @api.model + def _fix_debit_credit_round_bug(self): + logger.info('START script _fix_debit_credit_round_bug') + moves = self.sudo().search([]) # sudo to search in all companies + bug_move_ids = [] + for move in moves: + buggy = False + for l in move.line_ids: + if not float_is_zero(l.debit, precision_digits=2): + debit_rounded = float_round(l.debit, precision_digits=2) + if float_compare(l.debit, debit_rounded, precision_digits=6): + logger.info('Bad move to fix ID %d company_id %d name %s ref %s date %s journal %s (line ID %d debit=%s)', move.id, move.company_id.id, move.name, move.ref, move.date, move.journal_id.code, l.id, l.debit) + buggy = True + break + else: + credit_rounded = float_round(l.credit, precision_digits=2) + if float_compare(l.credit, credit_rounded, precision_digits=6): + logger.info('Bad move to fix ID %d company_id %d name %s ref %s date %s journal %s (line ID %d credit=%s)', move.id, move.company_id.id, move.name, move.ref, move.date, move.journal_id.code, l.id, l.credit) + buggy = True + break + if buggy: + bug_move_ids.append(move.id) + bal = 0.0 + max_credit = (False, 0) + for l in move.line_ids: + if not float_is_zero(l.debit, precision_digits=2): + new_debit = float_round(l.debit, precision_digits=2) + self._cr.execute( + 'UPDATE account_move_line set debit=%s, balance=%s where id=%s', + (new_debit, new_debit, l.id)) + bal -= new_debit + elif not float_is_zero(l.credit, precision_digits=2): + new_credit = float_round(l.credit, precision_digits=2) + self._cr.execute( + 'UPDATE account_move_line set credit=%s, balance=%s where id=%s', + (new_credit, new_credit * -1, l.id)) + bal += new_credit + if new_credit > max_credit[1]: + max_credit = (l, new_credit) + if not float_is_zero(bal, precision_digits=2): + assert abs(bal) < 0.05 + l = max_credit[0] + new_credit = max_credit[1] + new_new_credit = float_round(new_credit - bal, precision_digits=2) + assert new_new_credit > 0 + self._cr.execute( + 'UPDATE account_move_line set credit=%s, balance=%s where id=%s', + (new_new_credit, new_new_credit * -1, l.id)) + logger.info('Move ID %d fixed', move.id) + logger.info('%d buggy moves fixed (IDs: %s)', len(bug_move_ids), bug_move_ids) + logger.info('END _fix_debit_credit_round_bug') + class AccountMoveLine(models.Model): _inherit = 'account.move.line' From bb4e5d377cfba3846daf0996b0d797364dd064f7 Mon Sep 17 00:00:00 2001 From: Benoit Date: Thu, 23 Jun 2016 15:50:43 +0200 Subject: [PATCH 19/46] [ADD] mail_usability module for improvements on mails --- mail_usability/__init__.py | 23 ++++++++++++++++ mail_usability/__openerp__.py | 46 +++++++++++++++++++++++++++++++ mail_usability/mail.py | 52 +++++++++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+) create mode 100644 mail_usability/__init__.py create mode 100644 mail_usability/__openerp__.py create mode 100644 mail_usability/mail.py diff --git a/mail_usability/__init__.py b/mail_usability/__init__.py new file mode 100644 index 00000000..b910c1b2 --- /dev/null +++ b/mail_usability/__init__.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Mail Usability module for Odoo +# Copyright (C) 2016 Akretion (http://www.akretion.com) +# @author Benoît Guillot +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from . import mail diff --git a/mail_usability/__openerp__.py b/mail_usability/__openerp__.py new file mode 100644 index 00000000..3be53b1a --- /dev/null +++ b/mail_usability/__openerp__.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Mail Usability module for Odoo +# Copyright (C) 2016 Akretion (http://www.akretion.com) +# @author Benoît Guillot +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + + +{ + 'name': 'Mail Usability', + 'version': '0.1', + 'category': 'Base', + 'license': 'AGPL-3', + 'summary': 'Usability improvements on mails', + 'description': """ +Mail Usability +============== + +Small usability improvements on mails: + +* remove link in mail footer + +* remove 'sent by' in notification footer + + """, + 'author': 'Akretion', + 'website': 'http://www.akretion.com', + 'depends': ['mail'], + 'data': [], + 'installable': True, +} diff --git a/mail_usability/mail.py b/mail_usability/mail.py new file mode 100644 index 00000000..ded5f5fd --- /dev/null +++ b/mail_usability/mail.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +############################################################################## +# +# Mail Usability module for Odoo +# Copyright (C) 2016 Akretion (http://www.akretion.com) +# @author Benoît Guillot +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp import models, api + + +class MailMail(models.Model): + _inherit = 'mail.mail' + + @api.model + def send_get_mail_body(self, mail, partner=None): + """ + Avoid unwanted links in mail footer. + """ + return mail.body_html + + +class MailNotification(models.Model): + _inherit = 'mail.notification' + + @api.cr_uid_id_context + def get_signature_footer( + self, cr, uid, user_id, res_model=None, res_id=None, context=None, + user_signature=True): + """ + Remove : "Sent by 'Your Company' using Odoo" from signature. + """ + footer = super(MailNotification, self).get_signature_footer( + cr, uid, user_id, res_model=res_model, res_id=res_id, + context=context, user_signature=user_signature) + footer = footer[:footer.find('\n\n
Sent by ')] + footer = footer[:footer.find(u'\n\n
Envoyé par ')] + return footer From 81b78b83ce7c8e3efd2fa76bbc75220e33e01598 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Tue, 11 Oct 2016 15:32:30 +0200 Subject: [PATCH 20/46] Set all modules as uninstallable --- mail_usability/__openerp__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mail_usability/__openerp__.py b/mail_usability/__openerp__.py index 3be53b1a..4e28ec8e 100644 --- a/mail_usability/__openerp__.py +++ b/mail_usability/__openerp__.py @@ -42,5 +42,5 @@ 'website': 'http://www.akretion.com', 'depends': ['mail'], 'data': [], - 'installable': True, + 'installable': False, } From 6dbbb98774b91ffd7410c4d6969ec99119def30f Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Tue, 11 Oct 2016 15:40:03 +0200 Subject: [PATCH 21/46] Mass rename from __openerp__.py to __manifest__.py --- mail_usability/{__openerp__.py => __manifest__.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename mail_usability/{__openerp__.py => __manifest__.py} (100%) diff --git a/mail_usability/__openerp__.py b/mail_usability/__manifest__.py similarity index 100% rename from mail_usability/__openerp__.py rename to mail_usability/__manifest__.py From 453b6ba626df50ab7bb0e0d03a104de8dbf24728 Mon Sep 17 00:00:00 2001 From: David Beal Date: Fri, 18 Nov 2016 10:37:43 +0100 Subject: [PATCH 22/46] IMP add icons --- mail_usability/static/description/icon.png | Bin 0 -> 6662 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 mail_usability/static/description/icon.png diff --git a/mail_usability/static/description/icon.png b/mail_usability/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..1a60d0ef970b6a9b19e742f3bb9f86cb17f52daa GIT binary patch literal 6662 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 Date: Fri, 2 Dec 2016 19:44:56 +0100 Subject: [PATCH 23/46] Port mail_usability to v10 --- mail_usability/__init__.py | 22 -------------- mail_usability/__manifest__.py | 31 +++++--------------- mail_usability/mail.py | 52 ---------------------------------- mail_usability/mail_view.xml | 22 ++++++++++++++ 4 files changed, 29 insertions(+), 98 deletions(-) delete mode 100644 mail_usability/mail.py create mode 100644 mail_usability/mail_view.xml diff --git a/mail_usability/__init__.py b/mail_usability/__init__.py index b910c1b2..40a96afc 100644 --- a/mail_usability/__init__.py +++ b/mail_usability/__init__.py @@ -1,23 +1 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# Mail Usability module for Odoo -# Copyright (C) 2016 Akretion (http://www.akretion.com) -# @author Benoît Guillot -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## - -from . import mail diff --git a/mail_usability/__manifest__.py b/mail_usability/__manifest__.py index 4e28ec8e..67058ca7 100644 --- a/mail_usability/__manifest__.py +++ b/mail_usability/__manifest__.py @@ -1,29 +1,12 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# Mail Usability module for Odoo -# Copyright (C) 2016 Akretion (http://www.akretion.com) -# @author Benoît Guillot -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## - +# © 2016 Akretion (http://www.akretion.com) +# @author Benoît Guillot +# @author Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { 'name': 'Mail Usability', - 'version': '0.1', + 'version': '10.0.1.0.0', 'category': 'Base', 'license': 'AGPL-3', 'summary': 'Usability improvements on mails', @@ -41,6 +24,6 @@ 'author': 'Akretion', 'website': 'http://www.akretion.com', 'depends': ['mail'], - 'data': [], - 'installable': False, + 'data': ['mail_view.xml'], + 'installable': True, } diff --git a/mail_usability/mail.py b/mail_usability/mail.py deleted file mode 100644 index ded5f5fd..00000000 --- a/mail_usability/mail.py +++ /dev/null @@ -1,52 +0,0 @@ -# -*- coding: utf-8 -*- -############################################################################## -# -# Mail Usability module for Odoo -# Copyright (C) 2016 Akretion (http://www.akretion.com) -# @author Benoît Guillot -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as -# published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -# -############################################################################## - -from openerp import models, api - - -class MailMail(models.Model): - _inherit = 'mail.mail' - - @api.model - def send_get_mail_body(self, mail, partner=None): - """ - Avoid unwanted links in mail footer. - """ - return mail.body_html - - -class MailNotification(models.Model): - _inherit = 'mail.notification' - - @api.cr_uid_id_context - def get_signature_footer( - self, cr, uid, user_id, res_model=None, res_id=None, context=None, - user_signature=True): - """ - Remove : "Sent by 'Your Company' using Odoo" from signature. - """ - footer = super(MailNotification, self).get_signature_footer( - cr, uid, user_id, res_model=res_model, res_id=res_id, - context=context, user_signature=user_signature) - footer = footer[:footer.find('\n\n
Sent by ')] - footer = footer[:footer.find(u'\n\n
Envoyé par ')] - return footer diff --git a/mail_usability/mail_view.xml b/mail_usability/mail_view.xml new file mode 100644 index 00000000..41944467 --- /dev/null +++ b/mail_usability/mail_view.xml @@ -0,0 +1,22 @@ + + + + + + + mail_usability.mail.tree + mail.mail + + + + + + + + + + + From d34f273fdb984029b4c22318272497f79bc723e3 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Thu, 22 Jun 2017 19:28:49 +0200 Subject: [PATCH 24/46] mail_usability: add intermediary level to notify_email parameter of res.partner --- mail_usability/__init__.py | 2 ++ mail_usability/__manifest__.py | 1 + mail_usability/mail.py | 43 ++++++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+) create mode 100644 mail_usability/mail.py diff --git a/mail_usability/__init__.py b/mail_usability/__init__.py index 40a96afc..213f276e 100644 --- a/mail_usability/__init__.py +++ b/mail_usability/__init__.py @@ -1 +1,3 @@ # -*- coding: utf-8 -*- + +from . import mail diff --git a/mail_usability/__manifest__.py b/mail_usability/__manifest__.py index 67058ca7..c2975fc8 100644 --- a/mail_usability/__manifest__.py +++ b/mail_usability/__manifest__.py @@ -20,6 +20,7 @@ * remove 'sent by' in notification footer +* add a new entry *All Messages Except Notifications* to the field *Receive Inbox Notifications by Email* of partners (becomes the default value) """, 'author': 'Akretion', 'website': 'http://www.akretion.com', diff --git a/mail_usability/mail.py b/mail_usability/mail.py new file mode 100644 index 00000000..ecbadc12 --- /dev/null +++ b/mail_usability/mail.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2016-2017 Akretion (http://www.akretion.com) +# @author: Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +from odoo import models, fields, api + + +class ResPartner(models.Model): + _inherit = 'res.partner' + + notify_email = fields.Selection( + selection_add=[ + ('all_except_notification', 'All Messages Except Notifications')], + default='all_except_notification') + + @api.multi + def _notify( + self, message, force_send=False, send_after_commit=True, + user_signature=True): + if message.message_type == 'notification': + message_sudo = message.sudo() + email_channels = message.channel_ids.filtered( + lambda channel: channel.email_send) + bad_email = message_sudo.author_id and\ + message_sudo.author_id.email or message.email_from + self.sudo().search([ + '|', + ('id', 'in', self.ids), + ('channel_ids', 'in', email_channels.ids), + ('email', '!=', bad_email), + ('notify_email', '=', 'always')])._notify_by_email( + message, force_send=force_send, + send_after_commit=send_after_commit, + user_signature=user_signature) + self._notify_by_chat(message) + return True + else: + return super(ResPartner, self)._notify( + message, force_send=force_send, + send_after_commit=send_after_commit, + user_signature=user_signature) From c3f4775a585f4af04b1d099cc5c43bb3d87a8fba Mon Sep 17 00:00:00 2001 From: David Beal Date: Wed, 22 Nov 2017 11:18:22 +0100 Subject: [PATCH 25/46] UPD Branding --- mail_usability/static/description/icon.png | Bin 6662 -> 9769 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/mail_usability/static/description/icon.png b/mail_usability/static/description/icon.png index 1a60d0ef970b6a9b19e742f3bb9f86cb17f52daa..c91da7980d9ac757c41eb45c5388e69281434b76 100644 GIT binary patch literal 9769 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 Date: Tue, 4 Sep 2018 15:50:02 +0200 Subject: [PATCH 26/46] [IMP] improve the wizard for testing email, allow to search on object and to send email for real check --- mail_usability/mail.py | 38 ++++++++++++++++++++++++++++++++++++ mail_usability/mail_view.xml | 19 ++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/mail_usability/mail.py b/mail_usability/mail.py index ecbadc12..2d867496 100644 --- a/mail_usability/mail.py +++ b/mail_usability/mail.py @@ -41,3 +41,41 @@ def _notify( message, force_send=force_send, send_after_commit=send_after_commit, user_signature=user_signature) + + +class TemplatePreview(models.TransientModel): + _inherit = "email_template.preview" + + res_id = fields.Integer(compute='_compute_res_id') + object_id = fields.Reference(selection='_reference_models') + + @api.model + def default_get(self, fields): + result = super(TemplatePreview, self).default_get(fields) + if result.get('model_id'): + model = self.env['ir.model'].browse(result['model_id']) + result['object_id'] = model.model + return result + + def _reference_models(self): + result = self.default_get(['model_id']) + if result.get('model_id'): + model = self.env['ir.model'].browse(result['model_id']) + return [(model.model, model.name)] + else: + models = self.env['ir.model'].search([('state', '!=', 'manual')]) + return [(model.model, model.name) + for model in models + if not model.model.startswith('ir.')] + + @api.depends('object_id') + def _compute_res_id(self): + for record in self: + if self.object_id: + record.res_id = self.object_id.id + + def send(self): + template = self.env['mail.template'].browse( + self._context['template_id']) + template.send_mail( + self.res_id, force_send=True, raise_exception=True) diff --git a/mail_usability/mail_view.xml b/mail_usability/mail_view.xml index 41944467..6bce9e39 100644 --- a/mail_usability/mail_view.xml +++ b/mail_usability/mail_view.xml @@ -19,4 +19,23 @@
+ + email_template.preview + + + + True + + + + +
+
+
+
From 7f64d3c33e2ae487c976148e06483a2c717b81e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20BEAU?= Date: Tue, 4 Dec 2018 11:55:57 +0100 Subject: [PATCH 27/46] [IMP] add readme, remove auto following when sending an email, use light version of email notification to avoid injecting useless link in the mail sent --- mail_usability/README.rst | 10 ++++++++++ mail_usability/__manifest__.py | 5 ++++- mail_usability/mail.py | 28 ++++++++++++++++++++++++++++ mail_usability/mail_data.xml | 15 +++++++++++++++ 4 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 mail_usability/README.rst create mode 100644 mail_usability/mail_data.xml diff --git a/mail_usability/README.rst b/mail_usability/README.rst new file mode 100644 index 00000000..ba083950 --- /dev/null +++ b/mail_usability/README.rst @@ -0,0 +1,10 @@ +# Mail Usability + +Take back the control on your email + +## Feature + +- do not follow automatically a object when sending an email +- add the option 'All Messages Except Notifications' on partner and use it by default to avoid sending unwanted mail to partner +- better email preview, allow to select between the whole database object and not only the last 10 +- use a light template version for notification without link (link should be explicit) diff --git a/mail_usability/__manifest__.py b/mail_usability/__manifest__.py index c2975fc8..0f4426a5 100644 --- a/mail_usability/__manifest__.py +++ b/mail_usability/__manifest__.py @@ -25,6 +25,9 @@ 'author': 'Akretion', 'website': 'http://www.akretion.com', 'depends': ['mail'], - 'data': ['mail_view.xml'], + 'data': [ + 'mail_view.xml', + 'mail_data.xml', + ], 'installable': True, } diff --git a/mail_usability/mail.py b/mail_usability/mail.py index 2d867496..db4e31f0 100644 --- a/mail_usability/mail.py +++ b/mail_usability/mail.py @@ -42,6 +42,17 @@ def _notify( send_after_commit=send_after_commit, user_signature=user_signature) + def _notify_by_email( + self, message, force_send=False, send_after_commit=True, + user_signature=True): + if not self._context.get('custom_layout'): + self = self.with_context( + custom_layout='mail_usability.mail_template_notification') + return super(ResPartner, self)._notify_by_email( + message, force_send=force_send, + send_after_commit=send_after_commit, + user_signature=user_signature) + class TemplatePreview(models.TransientModel): _inherit = "email_template.preview" @@ -79,3 +90,20 @@ def send(self): self._context['template_id']) template.send_mail( self.res_id, force_send=True, raise_exception=True) + + +class MailThread(models.AbstractModel): + _inherit = 'mail.thread' + + @api.multi + @api.returns('self', lambda value: value.id) + def message_post(self, body='', subject=None, message_type='notification', + subtype=None, parent_id=False, attachments=None, + content_subtype='html', **kwargs): + # Do not implicitly follow an object by just sending a message + return super(MailThread, + self.with_context(mail_create_nosubscribe=True) + ).message_post( + body=body, subject=subject, message_type=message_type, + subtype=subtype, parent_id=parent_id, attachments=attachments, + content_subtype=content_subtype, **kwargs) diff --git a/mail_usability/mail_data.xml b/mail_usability/mail_data.xml new file mode 100644 index 00000000..f5499f46 --- /dev/null +++ b/mail_usability/mail_data.xml @@ -0,0 +1,15 @@ + + + + + + + Notification Email + ${object.subject} + + + ${object.body | safe} + + + + From 0f35f8e92b574e65c095c68647459b98b4620f64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20BEAU?= Date: Tue, 8 Jan 2019 23:24:59 +0100 Subject: [PATCH 28/46] [REF] refactor the code to make it simplifier and avoid hacking the _notify method --- mail_usability/mail.py | 44 +++++++++++++++++------------------------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/mail_usability/mail.py b/mail_usability/mail.py index db4e31f0..9490f226 100644 --- a/mail_usability/mail.py +++ b/mail_usability/mail.py @@ -15,40 +15,30 @@ class ResPartner(models.Model): ('all_except_notification', 'All Messages Except Notifications')], default='all_except_notification') - @api.multi - def _notify( - self, message, force_send=False, send_after_commit=True, - user_signature=True): + def _should_be_notify_by_email(self, message): if message.message_type == 'notification': - message_sudo = message.sudo() - email_channels = message.channel_ids.filtered( - lambda channel: channel.email_send) - bad_email = message_sudo.author_id and\ - message_sudo.author_id.email or message.email_from - self.sudo().search([ - '|', - ('id', 'in', self.ids), - ('channel_ids', 'in', email_channels.ids), - ('email', '!=', bad_email), - ('notify_email', '=', 'always')])._notify_by_email( - message, force_send=force_send, - send_after_commit=send_after_commit, - user_signature=user_signature) - self._notify_by_chat(message) - return True + if self.notify_email == 'always': + return True + else: + return False else: - return super(ResPartner, self)._notify( - message, force_send=force_send, - send_after_commit=send_after_commit, - user_signature=user_signature) + return True def _notify_by_email( self, message, force_send=False, send_after_commit=True, user_signature=True): + + # use an empty layout for notification by default if not self._context.get('custom_layout'): self = self.with_context( custom_layout='mail_usability.mail_template_notification') - return super(ResPartner, self)._notify_by_email( + + # Filter the partner that should receive the notification + filtered_partners = self.filtered( + lambda p: p._should_be_notify_by_email(message) + ) + + return super(ResPartner, filtered_partners)._notify_by_email( message, force_send=force_send, send_after_commit=send_after_commit, user_signature=user_signature) @@ -100,7 +90,9 @@ class MailThread(models.AbstractModel): def message_post(self, body='', subject=None, message_type='notification', subtype=None, parent_id=False, attachments=None, content_subtype='html', **kwargs): - # Do not implicitly follow an object by just sending a message + if not 'mail_create_nosubscribe' in self._context: + # Do not implicitly follow an object by just sending a message + self = self.with_context(mail_create_nosubscribe=True) return super(MailThread, self.with_context(mail_create_nosubscribe=True) ).message_post( From aec5bb9c9173d7470ab3efe697448b4e9ac8bc69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20BEAU?= Date: Tue, 8 Jan 2019 23:25:42 +0100 Subject: [PATCH 29/46] [IMP] remove the fucking auto_delete!!! --- mail_usability/__init__.py | 1 + mail_usability/mail.py | 7 +++++++ mail_usability/mail_template.py | 12 ++++++++++++ 3 files changed, 20 insertions(+) create mode 100644 mail_usability/mail_template.py diff --git a/mail_usability/__init__.py b/mail_usability/__init__.py index 213f276e..f3658f6d 100644 --- a/mail_usability/__init__.py +++ b/mail_usability/__init__.py @@ -1,3 +1,4 @@ # -*- coding: utf-8 -*- from . import mail +from . import mail_template diff --git a/mail_usability/mail.py b/mail_usability/mail.py index 9490f226..56e361be 100644 --- a/mail_usability/mail.py +++ b/mail_usability/mail.py @@ -43,6 +43,13 @@ def _notify_by_email( send_after_commit=send_after_commit, user_signature=user_signature) + def _notify_prepare_email_values(self, message): + res = super(ResPartner, self)._notify_prepare_email_values(message) + # Never auto delete notification email + # fucking to hard to debug when message have been delete + res['auto_delete'] = False + return res + class TemplatePreview(models.TransientModel): _inherit = "email_template.preview" diff --git a/mail_usability/mail_template.py b/mail_usability/mail_template.py new file mode 100644 index 00000000..f59dfd46 --- /dev/null +++ b/mail_usability/mail_template.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from openerp import fields, models + + +class MailTemplate(models.Model): + _inherit = 'mail.template' + + auto_delete = fields.Boolean(default=False) From 39bee6e5f796ee2320260b6ee277edba609fb881 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20BEAU?= Date: Tue, 8 Jan 2019 23:27:23 +0100 Subject: [PATCH 30/46] [IMP] add some extra style css support and add a debugger mode. Update readme --- mail_usability/README.rst | 10 +++++++++ mail_usability/__init__.py | 1 + mail_usability/tools.py | 45 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 mail_usability/tools.py diff --git a/mail_usability/README.rst b/mail_usability/README.rst index ba083950..d2a77190 100644 --- a/mail_usability/README.rst +++ b/mail_usability/README.rst @@ -8,3 +8,13 @@ Take back the control on your email - add the option 'All Messages Except Notifications' on partner and use it by default to avoid sending unwanted mail to partner - better email preview, allow to select between the whole database object and not only the last 10 - use a light template version for notification without link (link should be explicit) +- add some additional style in the white list when santizing html field (see tools.py) +- make the email template by default not 'auto_delete' + +## TIPS + +Never, never tick the 'auto_delete' on mail template because it fucking hard to debug +and understand what have been sent (we should create a module with a crontask, that drop them latter) + +If the template of mail do not look like the same when saving it in odoo, maybe the sanitize style have drop some balise +please run odoo with "LOG_STYLE_SANITIZE=True odoo" to understand what have been drop, magic warning logger will tell you everthing diff --git a/mail_usability/__init__.py b/mail_usability/__init__.py index f3658f6d..330afb00 100644 --- a/mail_usability/__init__.py +++ b/mail_usability/__init__.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- from . import mail +from . import tools from . import mail_template diff --git a/mail_usability/tools.py b/mail_usability/tools.py new file mode 100644 index 00000000..7d435c41 --- /dev/null +++ b/mail_usability/tools.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo.tools.mail import _Cleaner +import os +import logging +_logger = logging.getLogger(__name__) + +_Cleaner._style_whitelist += [ + 'word-wrap', + 'display' + 'border-top', + 'border-bottom', + 'border-left', + 'border-right', + 'text-transform', + ] + + +if os.getenv('LOG_STYLE_SANITIZE'): + # Monkey patch the parse style method to debug + # the missing style + def parse_style(self, el): + attributes = el.attrib + styling = attributes.get('style') + if styling: + valid_styles = {} + styles = self._style_re.findall(styling) + for style in styles: + if style[0].lower() in self._style_whitelist: + valid_styles[style[0].lower()] = style[1] + # START HACK + else: + _logger.warning('Remove style %s %s', *style) + # END HACK + if valid_styles: + el.attrib['style'] = '; '.join( + '%s:%s' % (key, val) + for (key, val) in valid_styles.iteritems()) + else: + del el.attrib['style'] + import pdb; pdb.set_trace() + _Cleaner.parse_style = parse_style From e644b27b7a8c07092d737d935d341553b51e56df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20BEAU?= Date: Fri, 11 Jan 2019 10:56:31 +0100 Subject: [PATCH 31/46] [IMP] add record_id on mail.message to be able to access to the record --- mail_usability/__init__.py | 1 + mail_usability/mail_message.py | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 mail_usability/mail_message.py diff --git a/mail_usability/__init__.py b/mail_usability/__init__.py index 330afb00..b5cf9814 100644 --- a/mail_usability/__init__.py +++ b/mail_usability/__init__.py @@ -3,3 +3,4 @@ from . import mail from . import tools from . import mail_template +from . import mail_message diff --git a/mail_usability/mail_message.py b/mail_usability/mail_message.py new file mode 100644 index 00000000..7b0a39c6 --- /dev/null +++ b/mail_usability/mail_message.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from openerp import api, fields, models + + +class MailMessage(models.Model): + _inherit = 'mail.message' + + @property + def record_id(self): + # we do not use a reference field here as mail message + # are used everywhere and many model are not yet loaded + # so odoo raise exception + self.ensure_one() + return self.env[self.model].browse(self.res_id) From 39855d7b122dfa7a6281d99dd3b8d76a3c9226eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20BEAU?= Date: Fri, 11 Jan 2019 11:07:29 +0100 Subject: [PATCH 32/46] [REF] refactor the code in order to split it in several file --- mail_usability/__init__.py | 6 +- mail_usability/__manifest__.py | 5 +- mail_usability/{ => data}/mail_data.xml | 0 mail_usability/mail.py | 108 ------------------ mail_usability/models/__init__.py | 7 ++ mail_usability/models/mail.py | 26 +++++ mail_usability/{ => models}/mail_message.py | 0 mail_usability/{ => models}/mail_template.py | 0 mail_usability/models/res_partner.py | 51 +++++++++ mail_usability/{ => models}/tools.py | 0 mail_usability/views/mail_view.xml | 21 ++++ mail_usability/wizard/__init__.py | 3 + .../wizard/email_template_preview.py | 44 +++++++ .../email_template_preview_view.xml} | 21 +--- 14 files changed, 159 insertions(+), 133 deletions(-) rename mail_usability/{ => data}/mail_data.xml (100%) delete mode 100644 mail_usability/mail.py create mode 100644 mail_usability/models/__init__.py create mode 100644 mail_usability/models/mail.py rename mail_usability/{ => models}/mail_message.py (100%) rename mail_usability/{ => models}/mail_template.py (100%) create mode 100644 mail_usability/models/res_partner.py rename mail_usability/{ => models}/tools.py (100%) create mode 100644 mail_usability/views/mail_view.xml create mode 100644 mail_usability/wizard/__init__.py create mode 100644 mail_usability/wizard/email_template_preview.py rename mail_usability/{mail_view.xml => wizard/email_template_preview_view.xml} (52%) diff --git a/mail_usability/__init__.py b/mail_usability/__init__.py index b5cf9814..35e7c960 100644 --- a/mail_usability/__init__.py +++ b/mail_usability/__init__.py @@ -1,6 +1,4 @@ # -*- coding: utf-8 -*- -from . import mail -from . import tools -from . import mail_template -from . import mail_message +from . import models +from . import wizard diff --git a/mail_usability/__manifest__.py b/mail_usability/__manifest__.py index 0f4426a5..07550acf 100644 --- a/mail_usability/__manifest__.py +++ b/mail_usability/__manifest__.py @@ -26,8 +26,9 @@ 'website': 'http://www.akretion.com', 'depends': ['mail'], 'data': [ - 'mail_view.xml', - 'mail_data.xml', + 'views/mail_view.xml', + 'data/mail_data.xml', + 'wizard/email_template_preview_view.xml', ], 'installable': True, } diff --git a/mail_usability/mail_data.xml b/mail_usability/data/mail_data.xml similarity index 100% rename from mail_usability/mail_data.xml rename to mail_usability/data/mail_data.xml diff --git a/mail_usability/mail.py b/mail_usability/mail.py deleted file mode 100644 index 56e361be..00000000 --- a/mail_usability/mail.py +++ /dev/null @@ -1,108 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (C) 2016-2017 Akretion (http://www.akretion.com) -# @author: Alexis de Lattre -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - - -from odoo import models, fields, api - - -class ResPartner(models.Model): - _inherit = 'res.partner' - - notify_email = fields.Selection( - selection_add=[ - ('all_except_notification', 'All Messages Except Notifications')], - default='all_except_notification') - - def _should_be_notify_by_email(self, message): - if message.message_type == 'notification': - if self.notify_email == 'always': - return True - else: - return False - else: - return True - - def _notify_by_email( - self, message, force_send=False, send_after_commit=True, - user_signature=True): - - # use an empty layout for notification by default - if not self._context.get('custom_layout'): - self = self.with_context( - custom_layout='mail_usability.mail_template_notification') - - # Filter the partner that should receive the notification - filtered_partners = self.filtered( - lambda p: p._should_be_notify_by_email(message) - ) - - return super(ResPartner, filtered_partners)._notify_by_email( - message, force_send=force_send, - send_after_commit=send_after_commit, - user_signature=user_signature) - - def _notify_prepare_email_values(self, message): - res = super(ResPartner, self)._notify_prepare_email_values(message) - # Never auto delete notification email - # fucking to hard to debug when message have been delete - res['auto_delete'] = False - return res - - -class TemplatePreview(models.TransientModel): - _inherit = "email_template.preview" - - res_id = fields.Integer(compute='_compute_res_id') - object_id = fields.Reference(selection='_reference_models') - - @api.model - def default_get(self, fields): - result = super(TemplatePreview, self).default_get(fields) - if result.get('model_id'): - model = self.env['ir.model'].browse(result['model_id']) - result['object_id'] = model.model - return result - - def _reference_models(self): - result = self.default_get(['model_id']) - if result.get('model_id'): - model = self.env['ir.model'].browse(result['model_id']) - return [(model.model, model.name)] - else: - models = self.env['ir.model'].search([('state', '!=', 'manual')]) - return [(model.model, model.name) - for model in models - if not model.model.startswith('ir.')] - - @api.depends('object_id') - def _compute_res_id(self): - for record in self: - if self.object_id: - record.res_id = self.object_id.id - - def send(self): - template = self.env['mail.template'].browse( - self._context['template_id']) - template.send_mail( - self.res_id, force_send=True, raise_exception=True) - - -class MailThread(models.AbstractModel): - _inherit = 'mail.thread' - - @api.multi - @api.returns('self', lambda value: value.id) - def message_post(self, body='', subject=None, message_type='notification', - subtype=None, parent_id=False, attachments=None, - content_subtype='html', **kwargs): - if not 'mail_create_nosubscribe' in self._context: - # Do not implicitly follow an object by just sending a message - self = self.with_context(mail_create_nosubscribe=True) - return super(MailThread, - self.with_context(mail_create_nosubscribe=True) - ).message_post( - body=body, subject=subject, message_type=message_type, - subtype=subtype, parent_id=parent_id, attachments=attachments, - content_subtype=content_subtype, **kwargs) diff --git a/mail_usability/models/__init__.py b/mail_usability/models/__init__.py new file mode 100644 index 00000000..342436c1 --- /dev/null +++ b/mail_usability/models/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- + +from . import mail +from . import tools +from . import mail_template +from . import mail_message +from . import res_partner diff --git a/mail_usability/models/mail.py b/mail_usability/models/mail.py new file mode 100644 index 00000000..41634991 --- /dev/null +++ b/mail_usability/models/mail.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2016-2017 Akretion (http://www.akretion.com) +# @author: Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + + +from odoo import models, api + + +class MailThread(models.AbstractModel): + _inherit = 'mail.thread' + + @api.multi + @api.returns('self', lambda value: value.id) + def message_post(self, body='', subject=None, message_type='notification', + subtype=None, parent_id=False, attachments=None, + content_subtype='html', **kwargs): + if not 'mail_create_nosubscribe' in self._context: + # Do not implicitly follow an object by just sending a message + self = self.with_context(mail_create_nosubscribe=True) + return super(MailThread, + self.with_context(mail_create_nosubscribe=True) + ).message_post( + body=body, subject=subject, message_type=message_type, + subtype=subtype, parent_id=parent_id, attachments=attachments, + content_subtype=content_subtype, **kwargs) diff --git a/mail_usability/mail_message.py b/mail_usability/models/mail_message.py similarity index 100% rename from mail_usability/mail_message.py rename to mail_usability/models/mail_message.py diff --git a/mail_usability/mail_template.py b/mail_usability/models/mail_template.py similarity index 100% rename from mail_usability/mail_template.py rename to mail_usability/models/mail_template.py diff --git a/mail_usability/models/res_partner.py b/mail_usability/models/res_partner.py new file mode 100644 index 00000000..f45a10bf --- /dev/null +++ b/mail_usability/models/res_partner.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2016-2019 Akretion (http://www.akretion.com) +# @author Sébastien BEAU +# @author: Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import models, fields + + +class ResPartner(models.Model): + _inherit = 'res.partner' + + notify_email = fields.Selection( + selection_add=[ + ('all_except_notification', 'All Messages Except Notifications')], + default='all_except_notification') + + def _should_be_notify_by_email(self, message): + if message.message_type == 'notification': + if self.notify_email == 'always': + return True + else: + return False + else: + return True + + def _notify_by_email( + self, message, force_send=False, send_after_commit=True, + user_signature=True): + + # use an empty layout for notification by default + if not self._context.get('custom_layout'): + self = self.with_context( + custom_layout='mail_usability.mail_template_notification') + + # Filter the partner that should receive the notification + filtered_partners = self.filtered( + lambda p: p._should_be_notify_by_email(message) + ) + + return super(ResPartner, filtered_partners)._notify_by_email( + message, force_send=force_send, + send_after_commit=send_after_commit, + user_signature=user_signature) + + def _notify_prepare_email_values(self, message): + res = super(ResPartner, self)._notify_prepare_email_values(message) + # Never auto delete notification email + # fucking to hard to debug when message have been delete + res['auto_delete'] = False + return res diff --git a/mail_usability/tools.py b/mail_usability/models/tools.py similarity index 100% rename from mail_usability/tools.py rename to mail_usability/models/tools.py diff --git a/mail_usability/views/mail_view.xml b/mail_usability/views/mail_view.xml new file mode 100644 index 00000000..6cbdc3f1 --- /dev/null +++ b/mail_usability/views/mail_view.xml @@ -0,0 +1,21 @@ + + + + + + + mail.mail + + + + + + + + + + + diff --git a/mail_usability/wizard/__init__.py b/mail_usability/wizard/__init__.py new file mode 100644 index 00000000..39d98f02 --- /dev/null +++ b/mail_usability/wizard/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import email_template_preview diff --git a/mail_usability/wizard/email_template_preview.py b/mail_usability/wizard/email_template_preview.py new file mode 100644 index 00000000..06df8165 --- /dev/null +++ b/mail_usability/wizard/email_template_preview.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# Copyright 2019 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from openerp import api, fields, models + + +class TemplatePreview(models.TransientModel): + _inherit = "email_template.preview" + + res_id = fields.Integer(compute='_compute_res_id') + object_id = fields.Reference(selection='_reference_models') + + @api.model + def default_get(self, fields): + result = super(TemplatePreview, self).default_get(fields) + if result.get('model_id'): + model = self.env['ir.model'].browse(result['model_id']) + result['object_id'] = model.model + return result + + def _reference_models(self): + result = self.default_get(['model_id']) + if result.get('model_id'): + model = self.env['ir.model'].browse(result['model_id']) + return [(model.model, model.name)] + else: + models = self.env['ir.model'].search([('state', '!=', 'manual')]) + return [(model.model, model.name) + for model in models + if not model.model.startswith('ir.')] + + @api.depends('object_id') + def _compute_res_id(self): + for record in self: + if self.object_id: + record.res_id = self.object_id.id + + def send(self): + template = self.env['mail.template'].browse( + self._context['template_id']) + template.send_mail( + self.res_id, force_send=True, raise_exception=True) diff --git a/mail_usability/mail_view.xml b/mail_usability/wizard/email_template_preview_view.xml similarity index 52% rename from mail_usability/mail_view.xml rename to mail_usability/wizard/email_template_preview_view.xml index 6bce9e39..494b92e9 100644 --- a/mail_usability/mail_view.xml +++ b/mail_usability/wizard/email_template_preview_view.xml @@ -1,24 +1,6 @@ - - - + - - mail_usability.mail.tree - mail.mail - - - - - - - - - - email_template.preview @@ -38,4 +20,5 @@ + From b62f3a35704c9f52e5dfb9426483db55d668259b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20BEAU?= Date: Tue, 19 Feb 2019 23:05:32 +0100 Subject: [PATCH 33/46] [IMP] by default do not send an email when user_id is fill on object --- mail_usability/models/mail.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/mail_usability/models/mail.py b/mail_usability/models/mail.py index 41634991..a17c91a9 100644 --- a/mail_usability/models/mail.py +++ b/mail_usability/models/mail.py @@ -5,11 +5,24 @@ from odoo import models, api +import logging +_logger = logging.getLogger(__name__) class MailThread(models.AbstractModel): _inherit = 'mail.thread' + def _active_message_auto_subscribe_notify(self): + _logger.debug('Skip automatic subscribe notification') + return False + + def _message_auto_subscribe_notify(self, partner_ids): + if self._active_message_auto_subscribe_notify(): + return super(MailThread, self)._message_auto_subscribe_notify( + partner_ids) + else: + return True + @api.multi @api.returns('self', lambda value: value.id) def message_post(self, body='', subject=None, message_type='notification', From ea4c6df7d6658e20a4f2b7917fc5449807630b31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20BEAU?= Date: Mon, 25 Feb 2019 15:57:56 +0100 Subject: [PATCH 34/46] [FIX] fix helper --- mail_usability/models/mail_message.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mail_usability/models/mail_message.py b/mail_usability/models/mail_message.py index 7b0a39c6..f046bb52 100644 --- a/mail_usability/models/mail_message.py +++ b/mail_usability/models/mail_message.py @@ -14,5 +14,7 @@ def record_id(self): # we do not use a reference field here as mail message # are used everywhere and many model are not yet loaded # so odoo raise exception - self.ensure_one() - return self.env[self.model].browse(self.res_id) + if self: + self.ensure_one() + return self.env[self.model].browse(self.res_id) + return None From 70d7cbdfda4c0e23f384dedfc5cace5b48c75c92 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Fri, 17 May 2019 20:27:52 +0200 Subject: [PATCH 35/46] New module link_tracker_usability --- mail_usability/models/res_partner.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mail_usability/models/res_partner.py b/mail_usability/models/res_partner.py index f45a10bf..dfbf6935 100644 --- a/mail_usability/models/res_partner.py +++ b/mail_usability/models/res_partner.py @@ -14,6 +14,7 @@ class ResPartner(models.Model): selection_add=[ ('all_except_notification', 'All Messages Except Notifications')], default='all_except_notification') + opt_out = fields.Boolean(track_visibility='onchange') def _should_be_notify_by_email(self, message): if message.message_type == 'notification': From f752fbc9f3e7390e9fa5fcaea9fc771cf5612ff3 Mon Sep 17 00:00:00 2001 From: Chafique Date: Mon, 26 Oct 2020 11:30:23 +0100 Subject: [PATCH 36/46] [12.0][MIG] mail_usability --- mail_usability/__init__.py | 2 - mail_usability/__manifest__.py | 7 ++- mail_usability/data/mail_data.xml | 6 +-- mail_usability/models/__init__.py | 2 - mail_usability/models/mail.py | 13 ++--- mail_usability/models/mail_message.py | 5 +- mail_usability/models/mail_template.py | 3 +- mail_usability/models/res_partner.py | 52 +++++-------------- mail_usability/models/tools.py | 4 +- mail_usability/views/mail_view.xml | 25 ++++----- mail_usability/wizard/__init__.py | 2 - .../wizard/email_template_preview.py | 11 ++-- .../wizard/email_template_preview_view.xml | 36 ++++++------- 13 files changed, 61 insertions(+), 107 deletions(-) diff --git a/mail_usability/__init__.py b/mail_usability/__init__.py index 35e7c960..9b429614 100644 --- a/mail_usability/__init__.py +++ b/mail_usability/__init__.py @@ -1,4 +1,2 @@ -# -*- coding: utf-8 -*- - from . import models from . import wizard diff --git a/mail_usability/__manifest__.py b/mail_usability/__manifest__.py index 07550acf..ca3c576a 100644 --- a/mail_usability/__manifest__.py +++ b/mail_usability/__manifest__.py @@ -1,12 +1,11 @@ -# -*- coding: utf-8 -*- -# © 2016 Akretion (http://www.akretion.com) +# Copyright 2020 Akretion France (http://www.akretion.com) # @author Benoît Guillot # @author Alexis de Lattre # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { 'name': 'Mail Usability', - 'version': '10.0.1.0.0', + 'version': '12.0.1.0.0', 'category': 'Base', 'license': 'AGPL-3', 'summary': 'Usability improvements on mails', @@ -29,6 +28,6 @@ 'views/mail_view.xml', 'data/mail_data.xml', 'wizard/email_template_preview_view.xml', - ], + ], 'installable': True, } diff --git a/mail_usability/data/mail_data.xml b/mail_usability/data/mail_data.xml index f5499f46..538fb44c 100644 --- a/mail_usability/data/mail_data.xml +++ b/mail_usability/data/mail_data.xml @@ -1,7 +1,6 @@ - - - + + Notification Email @@ -11,5 +10,4 @@ ${object.body | safe} - diff --git a/mail_usability/models/__init__.py b/mail_usability/models/__init__.py index 342436c1..030741b3 100644 --- a/mail_usability/models/__init__.py +++ b/mail_usability/models/__init__.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from . import mail from . import tools from . import mail_template diff --git a/mail_usability/models/mail.py b/mail_usability/models/mail.py index a17c91a9..2fca1f2e 100644 --- a/mail_usability/models/mail.py +++ b/mail_usability/models/mail.py @@ -1,5 +1,4 @@ -# -*- coding: utf-8 -*- -# Copyright (C) 2016-2017 Akretion (http://www.akretion.com) +# Copyright 2016-2017 Akretion France (http://www.akretion.com) # @author: Alexis de Lattre # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). @@ -31,9 +30,7 @@ def message_post(self, body='', subject=None, message_type='notification', if not 'mail_create_nosubscribe' in self._context: # Do not implicitly follow an object by just sending a message self = self.with_context(mail_create_nosubscribe=True) - return super(MailThread, - self.with_context(mail_create_nosubscribe=True) - ).message_post( - body=body, subject=subject, message_type=message_type, - subtype=subtype, parent_id=parent_id, attachments=attachments, - content_subtype=content_subtype, **kwargs) + return super(MailThread, self).message_post( + body=body, subject=subject, message_type=message_type, + subtype=subtype, parent_id=parent_id, attachments=attachments, + content_subtype=content_subtype, **kwargs) diff --git a/mail_usability/models/mail_message.py b/mail_usability/models/mail_message.py index f046bb52..8c97754f 100644 --- a/mail_usability/models/mail_message.py +++ b/mail_usability/models/mail_message.py @@ -1,9 +1,8 @@ -# -*- coding: utf-8 -*- -# Copyright 2019 Akretion (http://www.akretion.com). +# Copyright 2019 Akretion France (http://www.akretion.com) # @author Sébastien BEAU # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from openerp import api, fields, models +from odoo import models class MailMessage(models.Model): diff --git a/mail_usability/models/mail_template.py b/mail_usability/models/mail_template.py index f59dfd46..1cfdb0ce 100644 --- a/mail_usability/models/mail_template.py +++ b/mail_usability/models/mail_template.py @@ -1,5 +1,4 @@ -# -*- coding: utf-8 -*- -# Copyright 2018 Akretion (http://www.akretion.com). +# Copyright 2018 Akretion France (http://www.akretion.com) # @author Sébastien BEAU # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). diff --git a/mail_usability/models/res_partner.py b/mail_usability/models/res_partner.py index dfbf6935..86100a7a 100644 --- a/mail_usability/models/res_partner.py +++ b/mail_usability/models/res_partner.py @@ -1,52 +1,28 @@ -# -*- coding: utf-8 -*- -# Copyright (C) 2016-2019 Akretion (http://www.akretion.com) +# Copyright 2016-2019 Akretion France (http://www.akretion.com) # @author Sébastien BEAU # @author: Alexis de Lattre # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import models, fields +from odoo import models, fields, api class ResPartner(models.Model): _inherit = 'res.partner' - notify_email = fields.Selection( - selection_add=[ - ('all_except_notification', 'All Messages Except Notifications')], - default='all_except_notification') opt_out = fields.Boolean(track_visibility='onchange') - def _should_be_notify_by_email(self, message): - if message.message_type == 'notification': - if self.notify_email == 'always': - return True - else: - return False - else: - return True - - def _notify_by_email( - self, message, force_send=False, send_after_commit=True, - user_signature=True): - - # use an empty layout for notification by default + @api.model + def _notify(self, message, rdata, record, force_send=False, + send_after_commit=True, model_description=False, + mail_auto_delete=True): + # use an empty layout for notification by default if not self._context.get('custom_layout'): - self = self.with_context( - custom_layout='mail_usability.mail_template_notification') - - # Filter the partner that should receive the notification - filtered_partners = self.filtered( - lambda p: p._should_be_notify_by_email(message) - ) - - return super(ResPartner, filtered_partners)._notify_by_email( - message, force_send=force_send, - send_after_commit=send_after_commit, - user_signature=user_signature) - - def _notify_prepare_email_values(self, message): - res = super(ResPartner, self)._notify_prepare_email_values(message) + self = self.with_context( + custom_layout='mail_usability.mail_template_notification') # Never auto delete notification email # fucking to hard to debug when message have been delete - res['auto_delete'] = False - return res + mail_auto_delete = False + return super(ResPartner, self)._notify( + message=message, rdata=rdata, record=record, + force_send=force_send, send_after_commit=send_after_commit, + model_description=model_description, mail_auto_delete=mail_auto_delete) diff --git a/mail_usability/models/tools.py b/mail_usability/models/tools.py index 7d435c41..4c52393c 100644 --- a/mail_usability/models/tools.py +++ b/mail_usability/models/tools.py @@ -1,5 +1,4 @@ -# -*- coding: utf-8 -*- -# Copyright 2018 Akretion (http://www.akretion.com). +# Copyright 2018 Akretion France (http://www.akretion.com) # @author Sébastien BEAU # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). @@ -41,5 +40,4 @@ def parse_style(self, el): for (key, val) in valid_styles.iteritems()) else: del el.attrib['style'] - import pdb; pdb.set_trace() _Cleaner.parse_style = parse_style diff --git a/mail_usability/views/mail_view.xml b/mail_usability/views/mail_view.xml index 6cbdc3f1..15161532 100644 --- a/mail_usability/views/mail_view.xml +++ b/mail_usability/views/mail_view.xml @@ -1,21 +1,16 @@ - - - - mail.mail - - - - - - + + mail.mail + + + + + + + - - + diff --git a/mail_usability/wizard/__init__.py b/mail_usability/wizard/__init__.py index 39d98f02..134743a9 100644 --- a/mail_usability/wizard/__init__.py +++ b/mail_usability/wizard/__init__.py @@ -1,3 +1 @@ -# -*- coding: utf-8 -*- - from . import email_template_preview diff --git a/mail_usability/wizard/email_template_preview.py b/mail_usability/wizard/email_template_preview.py index 06df8165..ae26a042 100644 --- a/mail_usability/wizard/email_template_preview.py +++ b/mail_usability/wizard/email_template_preview.py @@ -1,5 +1,4 @@ -# -*- coding: utf-8 -*- -# Copyright 2019 Akretion (http://www.akretion.com). +# Copyright 2019 Akretion France (http://www.akretion.com) # @author Sébastien BEAU # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). @@ -26,10 +25,10 @@ def _reference_models(self): model = self.env['ir.model'].browse(result['model_id']) return [(model.model, model.name)] else: - models = self.env['ir.model'].search([('state', '!=', 'manual')]) - return [(model.model, model.name) - for model in models - if not model.model.startswith('ir.')] + ir_models = self.env['ir.model'].search([('state', '!=', 'manual')]) + return [(ir_model.model, ir_model.name) + for ir_model in ir_models + if not ir_model.model.startswith('ir.')] @api.depends('object_id') def _compute_res_id(self): diff --git a/mail_usability/wizard/email_template_preview_view.xml b/mail_usability/wizard/email_template_preview_view.xml index 494b92e9..1d64c9e6 100644 --- a/mail_usability/wizard/email_template_preview_view.xml +++ b/mail_usability/wizard/email_template_preview_view.xml @@ -1,24 +1,24 @@ - - email_template.preview - - - - True + + email_template.preview + + + + True + + + + +
+
- - - -
-
-
-
+
From 8449c3de80b808534985761b462f7519c1f85703 Mon Sep 17 00:00:00 2001 From: Chafique Date: Mon, 26 Oct 2020 11:36:04 +0100 Subject: [PATCH 37/46] [FIX] remove notify_email option in README.rst --- mail_usability/README.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/mail_usability/README.rst b/mail_usability/README.rst index d2a77190..ebac02d7 100644 --- a/mail_usability/README.rst +++ b/mail_usability/README.rst @@ -5,7 +5,6 @@ Take back the control on your email ## Feature - do not follow automatically a object when sending an email -- add the option 'All Messages Except Notifications' on partner and use it by default to avoid sending unwanted mail to partner - better email preview, allow to select between the whole database object and not only the last 10 - use a light template version for notification without link (link should be explicit) - add some additional style in the white list when santizing html field (see tools.py) From d05b75dbe4c3713e26db18f92b1b2873c755c377 Mon Sep 17 00:00:00 2001 From: Benoit Date: Thu, 18 Feb 2021 10:22:35 +0100 Subject: [PATCH 38/46] fix migraiton of mail_usability --- mail_usability/data/mail_data.xml | 5 +++++ mail_usability/models/mail.py | 4 ++-- mail_usability/models/res_partner.py | 17 ++++++++--------- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/mail_usability/data/mail_data.xml b/mail_usability/data/mail_data.xml index 538fb44c..0b510e43 100644 --- a/mail_usability/data/mail_data.xml +++ b/mail_usability/data/mail_data.xml @@ -10,4 +10,9 @@ ${object.body | safe} + + + diff --git a/mail_usability/models/mail.py b/mail_usability/models/mail.py index 2fca1f2e..d8a92c78 100644 --- a/mail_usability/models/mail.py +++ b/mail_usability/models/mail.py @@ -15,10 +15,10 @@ def _active_message_auto_subscribe_notify(self): _logger.debug('Skip automatic subscribe notification') return False - def _message_auto_subscribe_notify(self, partner_ids): + def _message_auto_subscribe_notify(self, partner_ids, template): if self._active_message_auto_subscribe_notify(): return super(MailThread, self)._message_auto_subscribe_notify( - partner_ids) + partner_ids, template) else: return True diff --git a/mail_usability/models/res_partner.py b/mail_usability/models/res_partner.py index 86100a7a..994df153 100644 --- a/mail_usability/models/res_partner.py +++ b/mail_usability/models/res_partner.py @@ -13,16 +13,15 @@ class ResPartner(models.Model): @api.model def _notify(self, message, rdata, record, force_send=False, - send_after_commit=True, model_description=False, - mail_auto_delete=True): - # use an empty layout for notification by default - if not self._context.get('custom_layout'): - self = self.with_context( - custom_layout='mail_usability.mail_template_notification') + send_after_commit=True, model_description=False, + mail_auto_delete=True): + # use an empty layout for notification by default + if not message.layout: + message.layout = 'mail_usability.message_notification_email_usability' # Never auto delete notification email # fucking to hard to debug when message have been delete mail_auto_delete = False return super(ResPartner, self)._notify( - message=message, rdata=rdata, record=record, - force_send=force_send, send_after_commit=send_after_commit, - model_description=model_description, mail_auto_delete=mail_auto_delete) + message=message, rdata=rdata, record=record, + force_send=force_send, send_after_commit=send_after_commit, + model_description=model_description, mail_auto_delete=mail_auto_delete) From 49226e814c90ec26cc46f23111ac4efa4cbca237 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Sun, 26 Sep 2021 23:39:07 +0200 Subject: [PATCH 39/46] stock_valuation_xlsx: add depreciation ratios --- stock_valuation_xlsx/__init__.py | 1 + stock_valuation_xlsx/__manifest__.py | 2 + stock_valuation_xlsx/models/__init__.py | 3 + .../models/stock_expiry_depreciation_rule.py | 36 +++++++ .../security/ir.model.access.csv | 3 + .../views/stock_expiry_depreciation_rule.xml | 35 ++++++ .../wizard/stock_valuation_xlsx.py | 101 +++++++++++++++--- .../wizard/stock_valuation_xlsx_view.xml | 2 + 8 files changed, 170 insertions(+), 13 deletions(-) create mode 100644 stock_valuation_xlsx/models/__init__.py create mode 100644 stock_valuation_xlsx/models/stock_expiry_depreciation_rule.py create mode 100644 stock_valuation_xlsx/security/ir.model.access.csv create mode 100644 stock_valuation_xlsx/views/stock_expiry_depreciation_rule.xml diff --git a/stock_valuation_xlsx/__init__.py b/stock_valuation_xlsx/__init__.py index 40272379..9b429614 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 97c78473..cc834442 100644 --- a/stock_valuation_xlsx/__manifest__.py +++ b/stock_valuation_xlsx/__manifest__.py @@ -37,9 +37,11 @@ 'website': 'http://www.akretion.com', 'depends': ['stock_account'], 'data': [ + 'security/ir.model.access.csv', 'wizard/stock_valuation_xlsx_view.xml', 'wizard/stock_variation_xlsx_view.xml', 'views/stock_inventory.xml', + 'views/stock_expiry_depreciation_rule.xml', ], 'installable': True, } diff --git a/stock_valuation_xlsx/models/__init__.py b/stock_valuation_xlsx/models/__init__.py new file mode 100644 index 00000000..caf40007 --- /dev/null +++ b/stock_valuation_xlsx/models/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import stock_expiry_depreciation_rule diff --git a/stock_valuation_xlsx/models/stock_expiry_depreciation_rule.py b/stock_valuation_xlsx/models/stock_expiry_depreciation_rule.py new file mode 100644 index 00000000..f94be27c --- /dev/null +++ b/stock_valuation_xlsx/models/stock_expiry_depreciation_rule.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 Akretion France (http://www.akretion.com/) +# @author: Alexis de Lattre +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class StockExpiryDepreciationRule(models.Model): + _name = 'stock.expiry.depreciation.rule' + _description = 'Stock Expiry Depreciation Rule' + _order = 'company_id, start_limit_days' + + company_id = fields.Many2one( + 'res.company', string='Company', + ondelete='cascade', required=True, + default=lambda self: self.env['res.company']._company_default_get()) + start_limit_days = fields.Integer( + string='Days Before/After Expiry', required=True, + help="Enter negative value for days before expiry. Enter positive values for days after expiry. This value is the START of the time interval when going from future to past.") + ratio = fields.Integer(string='Depreciation Ratio (%)', required=True) + name = fields.Char(string='Label') + + _sql_constraints = [( + 'ratio_positive', + 'CHECK(ratio >= 0)', + 'The depreciation ratio must be positive.' + ), ( + 'ratio_max', + 'CHECK(ratio <= 100)', + 'The depreciation ratio cannot be above 100%.' + ), ( + 'start_limit_days_unique', + 'unique(company_id, start_limit_days)', + 'This depreciation rule already exists in this company.' + )] diff --git a/stock_valuation_xlsx/security/ir.model.access.csv b/stock_valuation_xlsx/security/ir.model.access.csv new file mode 100644 index 00000000..fe37b4d9 --- /dev/null +++ b/stock_valuation_xlsx/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_stock_expiry_depreciation_rule_full,Full access on stock.expiry.depreciation.rule to account manager,model_stock_expiry_depreciation_rule,account.group_account_manager,1,1,1,1 +access_stock_expiry_depreciation_rule_read,Read access on stock.expiry.depreciation.rule to stock manager,model_stock_expiry_depreciation_rule,stock.group_stock_manager,1,0,0,0 diff --git a/stock_valuation_xlsx/views/stock_expiry_depreciation_rule.xml b/stock_valuation_xlsx/views/stock_expiry_depreciation_rule.xml new file mode 100644 index 00000000..6d0c784c --- /dev/null +++ b/stock_valuation_xlsx/views/stock_expiry_depreciation_rule.xml @@ -0,0 +1,35 @@ + + + + + + + + stock.expiry.depreciation.rule + + + + + + + + + + + + Stock Depreciation Rules + stock.expiry.depreciation.rule + tree + + + + + + diff --git a/stock_valuation_xlsx/wizard/stock_valuation_xlsx.py b/stock_valuation_xlsx/wizard/stock_valuation_xlsx.py index 8d48ad79..e0484608 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 @@ -67,11 +68,27 @@ class StockValuationXlsx(models.TransientModel): ('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') @@ -125,6 +142,17 @@ def get_product_ids(self): 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() @@ -179,17 +207,24 @@ def uom_id2name(self): return uom_id2name @api.model - def prodlot_id2name(self, product_ids): + 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 @@ -288,7 +323,7 @@ def group_result(self, data, split_by_lot, split_by_location): 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: @@ -297,17 +332,27 @@ def stringify_and_sort_result( 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']], )) @@ -326,6 +371,12 @@ def generate(self): 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.")) @@ -348,6 +399,13 @@ def generate(self): 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, @@ -355,11 +413,11 @@ def generate(self): data_res = self.group_result(data, split_by_lot, split_by_location) categ_id2name = self.product_categ_id2name(self.categ_ids) uom_id2name = self.uom_id2name() - lot_id2data = self.prodlot_id2name(in_stock_product_ids) + 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() @@ -372,12 +430,15 @@ def generate(self): 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']): @@ -433,6 +494,9 @@ def generate(self): 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: @@ -448,12 +512,20 @@ def generate(self): 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: @@ -519,6 +591,7 @@ def _prepare_styles(self, workbook, company, prec_price): '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({ @@ -543,8 +616,10 @@ def _prepare_cols(self): '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 3500d23c..bf90648f 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 @@ + + From b72d8c44d411233715443003047f396be439d837 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Mon, 15 Nov 2021 16:20:59 +0100 Subject: [PATCH 40/46] stock_valuation_xlsx: fix crash with using depreciation rules --- stock_valuation_xlsx/wizard/stock_valuation_xlsx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stock_valuation_xlsx/wizard/stock_valuation_xlsx.py b/stock_valuation_xlsx/wizard/stock_valuation_xlsx.py index e0484608..3b6892ad 100644 --- a/stock_valuation_xlsx/wizard/stock_valuation_xlsx.py +++ b/stock_valuation_xlsx/wizard/stock_valuation_xlsx.py @@ -145,7 +145,7 @@ def _prepare_product_fields(self): 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 + date_dt = fields.Date.to_date(past_date) # convert datetime to date else: date_dt = fields.Date.context_today(self) for rule in rules: From af806f7bcc9994e9233f809c507c75873886b9fe Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Fri, 21 Jan 2022 11:24:59 +0100 Subject: [PATCH 41/46] stock_usability: usability improvements on inventory and picking form view --- stock_usability/stock.py | 13 +++++++++++++ stock_usability/stock_view.xml | 19 ++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/stock_usability/stock.py b/stock_usability/stock.py index c0c67dd0..fdf858ca 100644 --- a/stock_usability/stock.py +++ b/stock_usability/stock.py @@ -146,3 +146,16 @@ def action_stock_move_lines_reserved(self): action = self.action_view_stock_moves() action['context'] = {'search_default_todo': True} return action + + +class StockInventoryLine(models.Model): + _inherit = 'stock.inventory.line' + + state = fields.Selection(store=True) + partner_id = fields.Many2one(states={'done': [('readonly', True)]}) + product_id = fields.Many2one(states={'done': [('readonly', True)]}) + product_uom_id = fields.Many2one(states={'done': [('readonly', True)]}) + product_qty = fields.Float(states={'done': [('readonly', True)]}) + location_id = fields.Many2one(states={'done': [('readonly', True)]}) + package_id = fields.Many2one(states={'done': [('readonly', True)]}) + prod_lot_id = fields.Many2one(states={'done': [('readonly', True)]}) diff --git a/stock_usability/stock_view.xml b/stock_usability/stock_view.xml index f408b3e4..8e52d9cd 100644 --- a/stock_usability/stock_view.xml +++ b/stock_usability/stock_view.xml @@ -23,6 +23,11 @@ + + + + @@ -350,7 +355,9 @@ should be able to access it. So I add a menu entry under Inventory Control. --> - + @@ -370,6 +377,16 @@ should be able to access it. So I add a menu entry under Inventory Control. --> + + stock.inventory.line + + + + {'readonly': ['|', ('product_tracking', '=', 'none'), ('state', '=', 'done')]} + + + + stock.usability.quant.tree stock.quant From 38a6b1221ee36543a27943e310662f5e079be799 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Fri, 4 Mar 2022 17:01:38 +0100 Subject: [PATCH 42/46] account_usability: add name_search on account.incoterms base_usability: Small improvement in script on res.users --- account_usability/account.py | 10 ++++++++++ base_usability/models/users.py | 6 +----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/account_usability/account.py b/account_usability/account.py index b238213c..65b4c8c6 100644 --- a/account_usability/account.py +++ b/account_usability/account.py @@ -724,6 +724,16 @@ def name_get(self): res.append((rec.id, '[%s] %s' % (rec.code, rec.name))) return res + @api.model + def name_search(self, name='', args=None, operator='ilike', limit=80): + if args is None: + args = [] + if name and operator == 'ilike': + recs = self.search([('code', '=', name)] + args, limit=limit) + if recs: + return recs.name_get() + return super().name_search(name=name, args=args, operator=operator, limit=limit) + class AccountReconciliation(models.AbstractModel): _inherit = 'account.reconciliation.widget' diff --git a/base_usability/models/users.py b/base_usability/models/users.py index ffebe32d..22f63e17 100644 --- a/base_usability/models/users.py +++ b/base_usability/models/users.py @@ -23,12 +23,9 @@ def default_get(self, fields_list): @api.model def _script_partners_linked_to_users_no_company(self): - if self.env.user.id != SUPERUSER_ID: - raise UserError(_('You must run this script as admin user')) logger.info( 'START to set company_id=False on partners related to users') - users = self.search( - ['|', ('active', '=', True), ('active', '=', False)]) + users = self.sudo().with_context(active_test=False).search([]) for user in users: if user.partner_id.company_id: user.partner_id.company_id = False @@ -37,4 +34,3 @@ def _script_partners_linked_to_users_no_company(self): user.login, user.id) logger.info( 'END setting company_id=False on partners related to users') - return True From 3b2efaab37afe3d519b7175ff3c9637aa59e54b2 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Fri, 6 May 2022 11:13:03 +0200 Subject: [PATCH 43/46] [ADD] module base_dynamic_list --- base_dynamic_list/__init__.py | 1 + base_dynamic_list/__manifest__.py | 62 +++++ base_dynamic_list/models/__init__.py | 1 + base_dynamic_list/models/dynamic_list.py | 115 +++++++++ .../security/ir.model.access.csv | 9 + base_dynamic_list/views/dynamic_list.xml | 240 ++++++++++++++++++ stock_valuation_xlsx/models/__init__.py | 2 - .../models/stock_expiry_depreciation_rule.py | 1 - 8 files changed, 428 insertions(+), 3 deletions(-) create mode 100644 base_dynamic_list/__init__.py create mode 100644 base_dynamic_list/__manifest__.py create mode 100644 base_dynamic_list/models/__init__.py create mode 100644 base_dynamic_list/models/dynamic_list.py create mode 100644 base_dynamic_list/security/ir.model.access.csv create mode 100644 base_dynamic_list/views/dynamic_list.xml diff --git a/base_dynamic_list/__init__.py b/base_dynamic_list/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/base_dynamic_list/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/base_dynamic_list/__manifest__.py b/base_dynamic_list/__manifest__.py new file mode 100644 index 00000000..4c664e95 --- /dev/null +++ b/base_dynamic_list/__manifest__.py @@ -0,0 +1,62 @@ +# Copyright 2020-2022 Akretion France (http://www.akretion.com) +# @author Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + 'name': 'Base Dynamic List', + 'version': '12.0.1.0.0', + 'category': 'Tools', + 'license': 'AGPL-3', + 'summary': 'Dynamic lists', + 'description': """ +Base Dynamic List +================= + +Very often during an Odoo implementation, we need to add selection fields on a native objet, and we don't want to have a hard-coded selection list (fields.Selection), but a selection list that can be changed by users (Many2one field). For that, the developper needs to add a new object (with just a 'name' and 'sequence' field) with a form/tree view. The goal of this module is to speed-up this process by defining a dynamic list object that already has all the required views. + +This module provides several ready-to-go objects: + +* simple list : fields *name*, *sequence* and *active* +* translatable list : fields *name* with translate=True, *sequence* and *active* +* code list : fields *code* (unique), *name*, *sequence* and *active* +* translatable code list : fields *code* (unique), *name* with translate=True, *sequence* and *active* + +These objects are readable by the employee group. The system group has full rights on it. + +To use it, you need to do 2 or 3 things : + +1) Add an entry in the domain field and the object you selected: + +domain = fields.Selection(selection_add=[('risk.type', "Risk Type")], ondelete={"risk.type": "cascade"}) + +2) Add the many2one field on your object: + +risk_type_id = fields.Many2one( + 'dynamic.list', string="Risk Type", + ondelete='restrict', domain=[('domain', '=', 'risk.type')]) + + +3) Optionally, you can add a dedicated action and a menu entry (otherwize, you can use the generic menu entry under *Settings > Technical > Dynamic Lists*: + + + Risk Type + dynamic.list + tree,form + [('domain', '=', 'risk.type')] + {'default_domain': 'risk.type'} + + + + +Limitation: when you want to have different access rights on these lists depending on the source object, you should prefer to use dedicated objects. +""", + 'author': 'Akretion', + 'website': 'http://www.akretion.com', + 'depends': ['base'], + 'data': [ + 'security/ir.model.access.csv', + 'views/dynamic_list.xml', + ], + 'installable': True, +} diff --git a/base_dynamic_list/models/__init__.py b/base_dynamic_list/models/__init__.py new file mode 100644 index 00000000..ab91119e --- /dev/null +++ b/base_dynamic_list/models/__init__.py @@ -0,0 +1 @@ +from . import dynamic_list diff --git a/base_dynamic_list/models/dynamic_list.py b/base_dynamic_list/models/dynamic_list.py new file mode 100644 index 00000000..4146cfb8 --- /dev/null +++ b/base_dynamic_list/models/dynamic_list.py @@ -0,0 +1,115 @@ +# Copyright 2020-2022 Akretion France (http://www.akretion.com) +# @author Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class DynamicList(models.Model): + _name = 'dynamic.list' + _description = 'Dynamic List (non translatable)' + _order = 'sequence, id' + + name = fields.Char(required=True) + sequence = fields.Integer(default=10) + active = fields.Boolean(default=True) + domain = fields.Selection([], string='Domain', required=True, index=True) + + _sql_constraint = [( + 'domain_name_uniq', + 'unique(domain, name)', + 'This entry already exists!' + )] + + +class DynamicListTranslate(models.Model): + _name = 'dynamic.list.translate' + _description = 'Translatable Dynamic List' + _order = 'sequence, id' + + name = fields.Char(translate=True, required=True) + sequence = fields.Integer(default=10) + active = fields.Boolean(default=True) + domain = fields.Selection([], string='Domain', required=True, index=True) + + _sql_constraint = [( + 'domain_name_uniq', + 'unique(domain, name)', + 'This entry already exists!' + )] + + +class DynamicListCode(models.Model): + _name = 'dynamic.list.code' + _description = 'Dynamic list with code' + _order = 'sequence, id' + + code = fields.Char(required=True) + name = fields.Char(translate=True, required=True) + sequence = fields.Integer(default=10) + active = fields.Boolean(default=True) + domain = fields.Selection([], string='Domain', required=True, index=True) + + _sql_constraint = [( + 'domain_code_uniq', + 'unique(domain, code)', + 'This code already exists!' + )] + + @api.depends('code', 'name') + def name_get(self): + res = [] + for rec in self: + res.append((rec.id, '[%s] %s' % (rec.code, rec.name))) + return res + + @api.model + def name_search( + self, name='', args=None, operator='ilike', limit=80): + if args is None: + args = [] + if name and operator == 'ilike': + recs = self.search( + [('code', '=', name)] + args, limit=limit) + if recs: + return recs.name_get() + return super().name_search( + name=name, args=args, operator=operator, limit=limit) + + +class DynamicListCodeTranslate(models.Model): + _name = 'dynamic.list.code.translate' + _description = 'Translatable dynamic list with code' + _order = 'sequence, id' + + code = fields.Char(required=True) + name = fields.Char(translate=True, required=True) + sequence = fields.Integer(default=10) + active = fields.Boolean(default=True) + domain = fields.Selection([], string='Domain', required=True, index=True) + + _sql_constraint = [( + 'domain_code_uniq', + 'unique(domain, code)', + 'This code already exists!' + )] + + @api.depends('code', 'name') + def name_get(self): + res = [] + for rec in self: + res.append((rec.id, '[%s] %s' % (rec.code, rec.name))) + return res + + @api.model + def name_search( + self, name='', args=None, operator='ilike', limit=80): + if args is None: + args = [] + if name and operator == 'ilike': + recs = self.search( + [('code', '=', name)] + args, limit=limit) + if recs: + return recs.name_get() + return super().name_search( + name=name, args=args, operator=operator, limit=limit) diff --git a/base_dynamic_list/security/ir.model.access.csv b/base_dynamic_list/security/ir.model.access.csv new file mode 100644 index 00000000..b7a626af --- /dev/null +++ b/base_dynamic_list/security/ir.model.access.csv @@ -0,0 +1,9 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_dynamic_list_read,Read access on dynamic.list to employees,model_dynamic_list,base.group_user,1,0,0,0 +access_dynamic_list_full,Full access to dynamic.list to System group,model_dynamic_list,base.group_system,1,1,1,1 +access_dynamic_list_translate_read,Read access on dynamic.list.translate to employees,model_dynamic_list_translate,base.group_user,1,0,0,0 +access_dynamic_list_translate_full,Full access to dynamic.list.translate to System group,model_dynamic_list_translate,base.group_system,1,1,1,1 +access_dynamic_list_code_read,Read access on dynamic.list.code to employees,model_dynamic_list_code,base.group_user,1,0,0,0 +access_dynamic_list_code_full,Full access to dynamic.list.code to System group,model_dynamic_list_code,base.group_system,1,1,1,1 +access_dynamic_list_code_translate_read,Read access on dynamic.list.code.translate to employees,model_dynamic_list_code_translate,base.group_user,1,0,0,0 +access_dynamic_list_code_translate_full,Full access to dynamic.list.code.translate to System group,model_dynamic_list_code_translate,base.group_system,1,1,1,1 diff --git a/base_dynamic_list/views/dynamic_list.xml b/base_dynamic_list/views/dynamic_list.xml new file mode 100644 index 00000000..28b2ca19 --- /dev/null +++ b/base_dynamic_list/views/dynamic_list.xml @@ -0,0 +1,240 @@ + + + + + + + + + + dynamic.list + +
+ +
+ +
+ + + + +
+
+
+
+ + + dynamic.list + + + + + + + + + + + dynamic.list + + + + + + + + + + + + + + Simple List + dynamic.list + tree,form + {'dynamic_list_main_view': True, 'search_default_domain_groupby': True} + + + + + + dynamic.list.translate + +
+ +
+ +
+ + + + +
+
+
+
+ + + dynamic.list.translate + + + + + + + + + + + dynamic.list.translate + + + + + + + + + + + + + + Translatable Simple List + dynamic.list.translate + tree,form + {'dynamic_list_translate_main_view': True, 'search_default_domain_groupby': True} + + + + + + dynamic.list.code + +
+ +
+ +
+ + + + + +
+
+
+
+ + + dynamic.list.code + + + + + + + + + + + + dynamic.list.code + + + + + + + + + + + + + + + Code List + dynamic.list.code + tree,form + {'dynamic_list_code_main_view': True, 'search_default_domain_groupby': True} + + + + + + dynamic.list.code.translate + +
+ +
+ +
+ + + + + +
+
+
+
+ + + dynamic.list.code.translate + + + + + + + + + + + + dynamic.list.code.translate + + + + + + + + + + + + + + + Translatable Code List + dynamic.list.code.translate + tree,form + {'dynamic_list_code_translate_main_view': True, 'search_default_domain_groupby': True} + + + + + +
diff --git a/stock_valuation_xlsx/models/__init__.py b/stock_valuation_xlsx/models/__init__.py index caf40007..eb4d7693 100644 --- a/stock_valuation_xlsx/models/__init__.py +++ b/stock_valuation_xlsx/models/__init__.py @@ -1,3 +1 @@ -# -*- coding: utf-8 -*- - from . import stock_expiry_depreciation_rule diff --git a/stock_valuation_xlsx/models/stock_expiry_depreciation_rule.py b/stock_valuation_xlsx/models/stock_expiry_depreciation_rule.py index f94be27c..e4389142 100644 --- a/stock_valuation_xlsx/models/stock_expiry_depreciation_rule.py +++ b/stock_valuation_xlsx/models/stock_expiry_depreciation_rule.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2021 Akretion France (http://www.akretion.com/) # @author: Alexis de Lattre # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). From 8fe2cac892a6372ee4fab9c75d7419181dbabcb9 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Fri, 6 May 2022 14:56:12 +0200 Subject: [PATCH 44/46] sale_report: add button to send order acknowledgement --- sale_usability/sale_view.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sale_usability/sale_view.xml b/sale_usability/sale_view.xml index 2b4bcb45..a310effe 100644 --- a/sale_usability/sale_view.xml +++ b/sale_usability/sale_view.xml @@ -26,6 +26,9 @@ +
From 0d689b1e957b04c8a876f3c7e55515ea8357d19a Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Fri, 13 May 2022 17:39:32 +0200 Subject: [PATCH 45/46] stock_valuation_xlsx: improve multi-company support Other small minor improvements/fixes --- stock_valuation_xlsx/__manifest__.py | 2 +- stock_valuation_xlsx/wizard/stock_valuation_xlsx.py | 10 +++++++--- stock_valuation_xlsx/wizard/stock_variation_xlsx.py | 4 ++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/stock_valuation_xlsx/__manifest__.py b/stock_valuation_xlsx/__manifest__.py index cc834442..721c46bc 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': '12.0.1.0.1', 'category': 'Tools', 'license': 'AGPL-3', 'summary': 'Generate XLSX reports for past or present stock levels', diff --git a/stock_valuation_xlsx/wizard/stock_valuation_xlsx.py b/stock_valuation_xlsx/wizard/stock_valuation_xlsx.py index 3b6892ad..d0ed12c7 100644 --- a/stock_valuation_xlsx/wizard/stock_valuation_xlsx.py +++ b/stock_valuation_xlsx/wizard/stock_valuation_xlsx.py @@ -157,7 +157,7 @@ def compute_product_data( self, company_id, in_stock_product_ids, standard_price_past_date=False): self.ensure_one() logger.debug('Start compute_product_data') - ppo = self.env['product.product'] + ppo = self.env['product.product'].with_context(force_company=company_id) ppho = self.env['product.price.history'] fields_list = self._prepare_product_fields() if not standard_price_past_date: @@ -396,9 +396,13 @@ def generate(self): elif self.source == 'inventory': past_date = self.inventory_id.date data, in_stock_products = self.compute_data_from_inventory(product_ids, prec_qty) - standard_price_past_date = past_date - if not (self.source == 'stock' and self.stock_date_type == 'present') and self.standard_price_date == 'present': + if self.source == 'stock' and self.stock_date_type == 'present': standard_price_past_date = False + else: # field standard_price_date is shown on screen + if self.standard_price_date == 'present': + standard_price_past_date = False + else: + standard_price_past_date = past_date depreciation_rules = [] if apply_depreciation: depreciation_rules = self._prepare_expiry_depreciation_rules(company_id, past_date) diff --git a/stock_valuation_xlsx/wizard/stock_variation_xlsx.py b/stock_valuation_xlsx/wizard/stock_variation_xlsx.py index 0215f04d..ae40cccb 100644 --- a/stock_valuation_xlsx/wizard/stock_variation_xlsx.py +++ b/stock_valuation_xlsx/wizard/stock_variation_xlsx.py @@ -122,7 +122,7 @@ def compute_product_data( standard_price_start_date=False, standard_price_end_date=False): self.ensure_one() logger.debug('Start compute_product_data') - ppo = self.env['product.product'] + ppo = self.env['product.product'].with_context(force_company=company_id) ppho = self.env['product.price.history'] fields_list = self._prepare_product_fields() if not standard_price_start_date or not standard_price_end_date: @@ -278,7 +278,7 @@ def generate(self): standard_price_start_date = standard_price_end_date = False if self.standard_price_start_date_type == 'start': standard_price_start_date = self.start_date - if self.standard_price_end_date_type == 'end': + if self.standard_price_end_date_type == 'end' and self.end_date_type == 'past': standard_price_end_date = self.end_date product_id2data = self.compute_product_data( From 0cd6ecd958d9b1a409070bd5191ea7f301c6efd9 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Thu, 16 Jun 2022 18:07:53 +0200 Subject: [PATCH 46/46] purchase_usability: warning when price and/or delay is auto-updated following a qty change It is similar to the feature present in sale_usability when the qty is updated and the price unit changes --- purchase_usability/purchase.py | 50 ++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/purchase_usability/purchase.py b/purchase_usability/purchase.py index ac42171d..51bfe422 100644 --- a/purchase_usability/purchase.py +++ b/purchase_usability/purchase.py @@ -3,8 +3,9 @@ # @author Alexis de Lattre # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import models, fields, api +from odoo import models, fields, api, _ from odoo.tools.misc import formatLang +from odoo.tools import float_compare class PurchaseOrder(models.Model): @@ -42,6 +43,51 @@ def name_get(self): if po.partner_ref: name += ' (' + po.partner_ref + ')' if self.env.context.get('show_total_amount') and po.amount_total: - name += ': ' + formatLang(self.env, po.amount_untaxed, currency_obj=po.currency_id) + name += ': ' + formatLang( + self.env, po.amount_untaxed, currency_obj=po.currency_id) result.append((po.id, name)) return result + + +class PurchaseOrderLine(models.Model): + _inherit = 'purchase.order.line' + + @api.onchange('product_qty', 'product_uom') + def _onchange_quantity(self): + # When the user has manually set a price and/or planned_date + # he is often upset when Odoo changes it when he changes the qty + # So we add a warning... + res = {} + old_price = self.price_unit + old_date_planned = self.date_planned + super()._onchange_quantity() + new_price = self.price_unit + new_date_planned = self.date_planned + prec = self.env['decimal.precision'].precision_get('Product Price') + price_compare = float_compare(old_price, new_price, precision_digits=prec) + if price_compare or old_date_planned != new_date_planned: + res['warning'] = { + 'title': _('Updates'), + 'message': _( + "Due to the update of the ordered quantity on line '%s', " + "the following data has been updated using the supplier info " + "of the product:" + ) % self.name + } + if price_compare: + res['warning']['message'] += _( + "\nOld price: %s\nNew price: %s") % ( + formatLang( + self.env, old_price, + currency_obj=self.order_id.currency_id), + formatLang( + self.env, new_price, + currency_obj=self.order_id.currency_id)) + + if old_date_planned != new_date_planned: + res['warning']['message'] += _( + "\nOld delivery date: %s\nNew delivery date: %s") % ( + old_date_planned, + new_date_planned, + ) + return res