diff --git a/account_usability/__manifest__.py b/account_usability/__manifest__.py index fed8cc94..0aa42fe1 100644 --- a/account_usability/__manifest__.py +++ b/account_usability/__manifest__.py @@ -31,6 +31,7 @@ 'views/account_report.xml', 'wizard/account_invoice_mark_sent_view.xml', 'wizard/account_group_generate_view.xml', + 'wizard/account_payment_register_views.xml', 'security/ir.model.access.csv', ], 'installable': True, diff --git a/account_usability/views/product.xml b/account_usability/views/product.xml index bc013c46..9bffdbe9 100644 --- a/account_usability/views/product.xml +++ b/account_usability/views/product.xml @@ -27,7 +27,7 @@ Here, we set all those fields on account.group_account_invoice - + diff --git a/account_usability/wizard/account_payment_register_views.xml b/account_usability/wizard/account_payment_register_views.xml new file mode 100644 index 00000000..26d09be5 --- /dev/null +++ b/account_usability/wizard/account_payment_register_views.xml @@ -0,0 +1,23 @@ + + + + + + + + account.payment.register + + + + + + + + + + diff --git a/base_partner_ref/__init__.py b/base_partner_ref/__init__.py index 4da81fa3..0650744f 100644 --- a/base_partner_ref/__init__.py +++ b/base_partner_ref/__init__.py @@ -1 +1 @@ -from . import partner +from . import models diff --git a/base_partner_ref/__manifest__.py b/base_partner_ref/__manifest__.py index 8e303bf8..dcf9a36c 100644 --- a/base_partner_ref/__manifest__.py +++ b/base_partner_ref/__manifest__.py @@ -1,10 +1,10 @@ -# Copyright 2017-2019 Akretion (http://www.akretion.com) +# Copyright 2017-2021 Akretion (http://www.akretion.com) # @author Alexis de Lattre # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { 'name': 'Base Partner Reference', - 'version': '12.0.1.0.0', + 'version': '14.0.1.0.0', 'category': 'Partner', 'license': 'AGPL-3', 'summary': "Improve usage of partner's Internal Reference", @@ -21,6 +21,6 @@ 'author': 'Akretion', 'website': 'http://www.akretion.com', 'depends': ['base'], - 'data': ['partner_view.xml'], - 'installable': False, + 'data': ['views/res_partner.xml'], + 'installable': True, } diff --git a/base_partner_ref/models/__init__.py b/base_partner_ref/models/__init__.py new file mode 100644 index 00000000..91fed54d --- /dev/null +++ b/base_partner_ref/models/__init__.py @@ -0,0 +1 @@ +from . import res_partner diff --git a/base_partner_ref/partner.py b/base_partner_ref/models/res_partner.py similarity index 80% rename from base_partner_ref/partner.py rename to base_partner_ref/models/res_partner.py index 4ada8b3a..2460c273 100644 --- a/base_partner_ref/partner.py +++ b/base_partner_ref/models/res_partner.py @@ -1,4 +1,4 @@ -# Copyright 2017-2019 Akretion +# Copyright 2017-2021 Akretion # @author: Alexis de Lattre # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). @@ -18,9 +18,9 @@ class ResPartner(models.Model): )] # add 'ref' in depends - @api.depends('is_company', 'name', 'parent_id.name', 'type', 'company_name', 'ref', 'invalidate_display_name') + @api.depends('ref', 'invalidate_display_name') def _compute_display_name(self): - super(ResPartner, self)._compute_display_name() + super()._compute_display_name() def _get_name(self): partner = self @@ -32,12 +32,13 @@ def _get_name(self): # END modif of native method if partner.company_name or partner.parent_id: if not name and partner.type in ['invoice', 'delivery', 'other']: - name = dict(self.fields_get(['type'])['type']['selection'])[partner.type] + name = dict(self.fields_get( + ['type'])['type']['selection'])[partner.type] if not partner.is_company: # START modif of native name_get() method company_name = partner.commercial_company_name or partner.parent_id.name if partner.parent_id.ref: - company_name = u"[%s] %s" % (partner.parent_id.ref, company_name) + company_name = "[%s] %s" % (partner.parent_id.ref, company_name) name = "%s, %s" % (company_name, name) # END modif of native name_get() method if self._context.get('show_address_only'): @@ -47,7 +48,8 @@ def _get_name(self): name = name.replace('\n\n', '\n') name = name.replace('\n\n', '\n') if self._context.get('address_inline'): - name = name.replace('\n', ', ') + splitted_names = name.split("\n") + name = ", ".join([n for n in splitted_names if n.strip()]) if self._context.get('show_email') and partner.email: name = "%s <%s>" % (name, partner.email) if self._context.get('html_format'): @@ -63,5 +65,6 @@ def name_search(self, name='', args=None, operator='ilike', limit=100): if name and operator == 'ilike': recs = self.search([('ref', '=', name)] + args, limit=limit) if recs: - return recs.name_get() + rec_childs = self.search([('id', 'child_of', recs.ids)]) + return rec_childs.name_get() return super().name_search(name=name, args=args, operator=operator, limit=limit) diff --git a/base_partner_ref/partner_view.xml b/base_partner_ref/views/res_partner.xml similarity index 83% rename from base_partner_ref/partner_view.xml rename to base_partner_ref/views/res_partner.xml index c18af954..6f5daa8b 100644 --- a/base_partner_ref/partner_view.xml +++ b/base_partner_ref/views/res_partner.xml @@ -11,29 +11,34 @@ Move ref in partner form to make it more visible res.partner + 1000 - + + 1 + + + - + 1 +--> Add ref in partner kanban view diff --git a/base_usability/views/res_partner.xml b/base_usability/views/res_partner.xml index 37c9510f..829560b9 100644 --- a/base_usability/views/res_partner.xml +++ b/base_usability/views/res_partner.xml @@ -39,6 +39,9 @@ res.partner + + + diff --git a/crm_usability/__init__.py b/crm_usability/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/crm_usability/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/crm_usability/__manifest__.py b/crm_usability/__manifest__.py new file mode 100644 index 00000000..e5394dc8 --- /dev/null +++ b/crm_usability/__manifest__.py @@ -0,0 +1,25 @@ +# Copyright 2016-2021 Akretion (http://www.akretion.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +# @author Alexis de Lattre + +{ + 'name': 'CRM Usability', + 'version': '14.0.1.0.0', + 'category': 'Customer Relationship Management', + 'license': 'AGPL-3', + 'summary': 'CRM usability enhancements', + 'description': """ +CRM Usability +============= + +This module has been written by Alexis de Lattre from Akretion +. + """, + 'author': 'Akretion', + 'website': 'http://www.akretion.com', + 'depends': ['crm'], + 'data': [ + 'views/crm_lead.xml', + ], + 'installable': True, +} diff --git a/crm_usability/models/__init__.py b/crm_usability/models/__init__.py new file mode 100644 index 00000000..e66f0d6c --- /dev/null +++ b/crm_usability/models/__init__.py @@ -0,0 +1 @@ +from . import crm_lead diff --git a/crm_usability/models/crm_lead.py b/crm_usability/models/crm_lead.py new file mode 100644 index 00000000..7cfc1e1a --- /dev/null +++ b/crm_usability/models/crm_lead.py @@ -0,0 +1,13 @@ +# Copyright 2017-2021 Akretion (http://www.akretion.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +# @author Alexis de Lattre + +from odoo import fields, models + + +class CrmLead(models.Model): + _inherit = 'crm.lead' + + probability = fields.Float(tracking=100) + date_deadline = fields.Date(tracking=110) + name = fields.Char(tracking=1) diff --git a/crm_usability/static/description/icon.png b/crm_usability/static/description/icon.png new file mode 100644 index 00000000..c91da798 Binary files /dev/null and b/crm_usability/static/description/icon.png differ diff --git a/crm_usability/views/crm_lead.xml b/crm_usability/views/crm_lead.xml new file mode 100644 index 00000000..d7218bd9 --- /dev/null +++ b/crm_usability/views/crm_lead.xml @@ -0,0 +1,22 @@ + + + + + + + + usability.crm.lead.opportunity.search + crm.lead + + + + + + + + + diff --git a/pos_no_product_template_menu/__init__.py b/link_tracker_usability/__init__.py similarity index 100% rename from pos_no_product_template_menu/__init__.py rename to link_tracker_usability/__init__.py diff --git a/link_tracker_usability/__manifest__.py b/link_tracker_usability/__manifest__.py new file mode 100644 index 00000000..3ab79e99 --- /dev/null +++ b/link_tracker_usability/__manifest__.py @@ -0,0 +1,27 @@ +# Copyright 2019-2021 Akretion France (http://www.akretion.com) +# @author Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + 'name': 'Link Tracker Usability', + 'version': '14.0.1.0.0', + 'category': 'Marketing', + 'license': 'AGPL-3', + 'summary': 'Improve usability for link tracker', + 'description': """ +Link Tracker Usability +====================== + +Several small usability improvements. + +This module has been written by Alexis de Lattre from Akretion +. + """, + 'author': 'Akretion', + 'website': 'http://www.akretion.com', + 'depends': ['link_tracker'], + 'data': [ + 'views/link_tracker_click.xml', + ], + 'installable': True, +} diff --git a/link_tracker_usability/views/link_tracker_click.xml b/link_tracker_usability/views/link_tracker_click.xml new file mode 100644 index 00000000..5361d2cb --- /dev/null +++ b/link_tracker_usability/views/link_tracker_click.xml @@ -0,0 +1,45 @@ + + + + + + + + usability.link.tracker.click.tree + link.tracker.click + + + + + + + + + + usability.link.tracker.click.form + link.tracker.click + + + + + + + + + + usability.link.tracker.click.search + link.tracker.click + + + + + + + + + + diff --git a/mass_mailing_usability/__init__.py b/mass_mailing_usability/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/mass_mailing_usability/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/mass_mailing_usability/__manifest__.py b/mass_mailing_usability/__manifest__.py new file mode 100644 index 00000000..56551831 --- /dev/null +++ b/mass_mailing_usability/__manifest__.py @@ -0,0 +1,29 @@ +# Copyright 2019-2021 Akretion France (http://www.akretion.com) +# @author Alexis de Lattre +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + 'name': 'Mass Mailing Campaigns Usability', + 'version': '14.0.1.0.0', + 'category': 'Marketing', + 'license': 'AGPL-3', + 'summary': 'Improve usability of mass mailing campaigns', + 'description': """ +Mass Mailing Campaigns Usability +================================ + +Several small usability improvements on the module mass_mailing: + +* show fields on link.tracker.click that are not displayed by default + +This module has been written by Alexis de Lattre from Akretion +. + """, + 'author': 'Akretion', + 'website': 'http://www.akretion.com', + 'depends': ['mass_mailing', 'link_tracker_usability'], + 'data': [ +# 'views/link_tracker.xml', + ], + 'installable': False, +} diff --git a/mass_mailing_usability/views/link_tracker.xml b/mass_mailing_usability/views/link_tracker.xml new file mode 100644 index 00000000..209097cd --- /dev/null +++ b/mass_mailing_usability/views/link_tracker.xml @@ -0,0 +1,48 @@ + + + + + + + + mm.usability.link.tracker.click.tree + link.tracker.click + + + + + + + + + + + mm.usability.link.tracker.click.form + link.tracker.click + + + + + + + + + + + + + mm.usability.link.tracker.click.search + link.tracker.click + + + + + + + + + diff --git a/pos_no_product_template_menu/__manifest__.py b/pos_no_product_template_menu/__manifest__.py deleted file mode 100644 index 3436e09c..00000000 --- a/pos_no_product_template_menu/__manifest__.py +++ /dev/null @@ -1,28 +0,0 @@ -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -{ - 'name': 'POS No Product Template Menu', - 'version': '12.0.1.0.0', - 'category': 'Point of sale', - 'license': 'AGPL-3', - 'summary': "Replace product.template menu entries by product.product menu", - 'description': """ -POS No Product Template -======================= - -This module replaces the menu entry for product.template by menu entries -for product.product in the *Point Of Sale > Product* menu. - -This module also switches to the tree view by default -for Product menu entries, instead of the kanban view. - -This module has been written by David Béal -from Akretion . - """, - 'author': 'Akretion', - 'website': 'http://www.akretion.com', - 'depends': ['point_of_sale', 'sale_purchase_no_product_template_menu'], - 'auto_install': True, - 'data': ['pos_view.xml'], - 'installable': False, -} diff --git a/pos_no_product_template_menu/pos_view.xml b/pos_no_product_template_menu/pos_view.xml deleted file mode 100644 index 89af7e56..00000000 --- a/pos_no_product_template_menu/pos_view.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - Products - product.product - kanban,tree,form - {'default_available_in_pos': True, 'search_default_filter_to_availabe_pos': 1} - - - - - - - diff --git a/product_usability/views/product_supplierinfo_view.xml b/product_usability/views/product_supplierinfo_view.xml index 6b75ced2..dea9868e 100644 --- a/product_usability/views/product_supplierinfo_view.xml +++ b/product_usability/views/product_supplierinfo_view.xml @@ -12,7 +12,7 @@ - + diff --git a/purchase_usability/i18n/fr.po b/purchase_usability/i18n/fr.po new file mode 100644 index 00000000..09fe8c84 --- /dev/null +++ b/purchase_usability/i18n/fr.po @@ -0,0 +1,208 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * purchase_usability +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-11-30 13:35+0000\n" +"PO-Revision-Date: 2021-11-30 13:35+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: purchase_usability +#: model_terms:ir.ui.view,arch_db:purchase_usability.purchase_order_line_search +msgid "Analytic Account" +msgstr "Compte Analytique" + +#. module: purchase_usability +#: model_terms:ir.ui.view,arch_db:purchase_usability.purchase_order_form +msgid "Are you sure you want to cancel this purchase order?" +msgstr "" + +#. module: purchase_usability +#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order_line__invoice_status +#: model_terms:ir.ui.view,arch_db:purchase_usability.purchase_order_line_search +#: model_terms:ir.ui.view,arch_db:purchase_usability.view_purchase_order_filter +msgid "Billing Status" +msgstr "État de facturation" + +#. module: purchase_usability +#: model_terms:ir.ui.view,arch_db:purchase_usability.purchase_order_line_search +msgid "Bills Received" +msgstr "Factures reçues" + +#. module: purchase_usability +#: model:ir.model,name:purchase_usability.model_res_partner +msgid "Contact" +msgstr "" + +#. module: purchase_usability +#: model:ir.model.fields,field_description:purchase_usability.field_product_product__purchase_method +#: model:ir.model.fields,field_description:purchase_usability.field_product_template__purchase_method +msgid "Control Policy" +msgstr "Politique de contrôle" + +#. module: purchase_usability +#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order__currency_id +msgid "Currency" +msgstr "Devise" + +#. module: purchase_usability +#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order__delivery_partner_id +msgid "Delivery Partner" +msgstr "" + +#. module: purchase_usability +#: model:ir.model.fields,field_description:purchase_usability.field_product_template__display_name +#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order__display_name +#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order_line__display_name +#: model:ir.model.fields,field_description:purchase_usability.field_res_partner__display_name +msgid "Display Name" +msgstr "Nom affiché" + +#. module: purchase_usability +#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order__dest_address_id +msgid "Drop Ship Address" +msgstr "Adresse de livraison directe" + +#. module: purchase_usability +#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order__fiscal_position_id +msgid "Fiscal Position" +msgstr "Position fiscale" + +#. module: purchase_usability +#: model:ir.model.fields.selection,name:purchase_usability.selection__purchase_order_line__invoice_status__invoiced +msgid "Fully Billed" +msgstr "Complètement facturé" + +#. module: purchase_usability +#: model:ir.model.fields,field_description:purchase_usability.field_product_template__id +#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order__id +#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order_line__id +#: model:ir.model.fields,field_description:purchase_usability.field_res_partner__id +msgid "ID" +msgstr "" + +#. module: purchase_usability +#: model:ir.model.fields,help:purchase_usability.field_purchase_order_line__product_barcode +msgid "International Article Number used for product identification." +msgstr "" + +#. module: purchase_usability +#: model:ir.model.fields,field_description:purchase_usability.field_product_template____last_update +#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order____last_update +#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order_line____last_update +#: model:ir.model.fields,field_description:purchase_usability.field_res_partner____last_update +msgid "Last Modified on" +msgstr "Dernière modification le" + +#. module: purchase_usability +#: model:ir.model.fields.selection,name:purchase_usability.selection__purchase_order_line__invoice_status__no +msgid "Nothing to Bill" +msgstr "Rien à facturer" + +#. module: purchase_usability +#: model:ir.model.fields,help:purchase_usability.field_product_product__purchase_method +#: model:ir.model.fields,help:purchase_usability.field_product_template__purchase_method +msgid "" +"On ordered quantities: Control bills based on ordered quantities.\n" +"On received quantities: Control bills based on received quantities." +msgstr "" +"Sur base des quantités commandées: factures de controle basées sur les quantités commandées. \n" +"Sur base des quantités reçues: factures de controle basées sur les quantités reçues." + +#. module: purchase_usability +#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order__payment_term_id +msgid "Payment Terms" +msgstr "Conditions de paiement" + +#. module: purchase_usability +#: model_terms:ir.ui.view,arch_db:purchase_usability.purchase_order_form +msgid "Print" +msgstr "Imprimer" + +#. module: purchase_usability +#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order_line__product_barcode +msgid "Product Barcode" +msgstr "Code-barre produit" + +#. module: purchase_usability +#: model:ir.model,name:purchase_usability.model_product_template +msgid "Product Template" +msgstr "Modèle d'article" + +#. module: purchase_usability +#: model:ir.model,name:purchase_usability.model_purchase_order +#: model:ir.model.fields,field_description:purchase_usability.field_res_partner__purchase_warn +#: model:ir.model.fields,field_description:purchase_usability.field_res_users__purchase_warn +msgid "Purchase Order" +msgstr "Commande fournisseur" + +#. module: purchase_usability +#: model:ir.model,name:purchase_usability.model_purchase_order_line +msgid "Purchase Order Line" +msgstr "Ligne de commande fournisseur" + +#. module: purchase_usability +#: model:ir.model.fields,field_description:purchase_usability.field_product_product__purchase_line_warn +#: model:ir.model.fields,field_description:purchase_usability.field_product_template__purchase_line_warn +msgid "Purchase Order Line Warning" +msgstr "Avertissement Ligne de Commande " + +#. module: purchase_usability +#: model:ir.model.fields,help:purchase_usability.field_purchase_order__dest_address_id +msgid "" +"Put an address if you want to deliver directly from the vendor to the " +"customer. Otherwise, keep empty to deliver to your own company." +msgstr "" +"Ajoutez une adresse si vous voulez livrer directement du fournisseur au " +"client. Sinon, laissez vide pour vous faire livrer à votre société." + +#. module: purchase_usability +#: model:ir.model.fields,help:purchase_usability.field_purchase_order__partner_ref +msgid "" +"Reference of the sales order or bid sent by the vendor. It's used to do the " +"matching when you receive the products as this reference is usually written " +"on the delivery order sent by your vendor." +msgstr "" +"Référence de la commande client ou offre envoyée par le fournisseur. Utilisé" +" principalement pour faire la correspondance lors de la réception des " +"articles, puisque cette référence est généralement écrite sur le bon de " +"livraison envoyé par votre fournisseur." + +#. module: purchase_usability +#: model_terms:ir.ui.view,arch_db:purchase_usability.view_purchase_order_filter +msgid "Reference, Origin or Vendor Reference" +msgstr "Référence, Origine ou Référence fournisseur" + +#. module: purchase_usability +#: model:ir.model.fields,help:purchase_usability.field_product_product__purchase_line_warn +#: model:ir.model.fields,help:purchase_usability.field_product_template__purchase_line_warn +#: model:ir.model.fields,help:purchase_usability.field_res_partner__purchase_warn +#: model:ir.model.fields,help:purchase_usability.field_res_users__purchase_warn +msgid "" +"Selecting the \"Warning\" option will notify user with the message, " +"Selecting \"Blocking Message\" will throw an exception with the message and " +"block the flow. The Message has to be written in the next field." +msgstr "" +"Sélectionner l'option 'Avertissement' notifiera l'utilisateur avec le " +"Message. Sélectionner 'Message Bloquant' lancera une exception avec le " +"message et bloquera le flux. Le Message doit être encodé dans le champ " +"suivant." + +#. module: purchase_usability +#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order__partner_ref +msgid "Vendor Reference" +msgstr "Référence fournisseur" + +#. module: purchase_usability +#: model:ir.model.fields.selection,name:purchase_usability.selection__purchase_order_line__invoice_status__to_invoice +#: model_terms:ir.ui.view,arch_db:purchase_usability.purchase_order_line_search +msgid "Waiting Bills" +msgstr "Factures en attente" diff --git a/purchase_usability/i18n/purchase_usability.pot b/purchase_usability/i18n/purchase_usability.pot new file mode 100644 index 00000000..22a1be68 --- /dev/null +++ b/purchase_usability/i18n/purchase_usability.pot @@ -0,0 +1,196 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * purchase_usability +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-11-02 09:44+0000\n" +"PO-Revision-Date: 2021-11-02 09:44+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: purchase_usability +#: model_terms:ir.ui.view,arch_db:purchase_usability.purchase_order_line_search +msgid "Analytic Account" +msgstr "" + +#. module: purchase_usability +#: model_terms:ir.ui.view,arch_db:purchase_usability.purchase_order_form +msgid "Are you sure you want to cancel this purchase order?" +msgstr "" + +#. module: purchase_usability +#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order_line__invoice_status +#: model_terms:ir.ui.view,arch_db:purchase_usability.purchase_order_line_search +#: model_terms:ir.ui.view,arch_db:purchase_usability.view_purchase_order_filter +msgid "Billing Status" +msgstr "" + +#. module: purchase_usability +#: model_terms:ir.ui.view,arch_db:purchase_usability.purchase_order_line_search +msgid "Bills Received" +msgstr "" + +#. module: purchase_usability +#: model:ir.model,name:purchase_usability.model_res_partner +msgid "Contact" +msgstr "" + +#. module: purchase_usability +#: model:ir.model.fields,field_description:purchase_usability.field_product_product__purchase_method +#: model:ir.model.fields,field_description:purchase_usability.field_product_template__purchase_method +msgid "Control Policy" +msgstr "" + +#. module: purchase_usability +#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order__currency_id +msgid "Currency" +msgstr "" + +#. module: purchase_usability +#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order__delivery_partner_id +msgid "Delivery Partner" +msgstr "" + +#. module: purchase_usability +#: model:ir.model.fields,field_description:purchase_usability.field_product_template__display_name +#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order__display_name +#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order_line__display_name +#: model:ir.model.fields,field_description:purchase_usability.field_res_partner__display_name +msgid "Display Name" +msgstr "" + +#. module: purchase_usability +#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order__dest_address_id +msgid "Drop Ship Address" +msgstr "" + +#. module: purchase_usability +#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order__fiscal_position_id +msgid "Fiscal Position" +msgstr "" + +#. module: purchase_usability +#: model:ir.model.fields.selection,name:purchase_usability.selection__purchase_order_line__invoice_status__invoiced +msgid "Fully Billed" +msgstr "" + +#. module: purchase_usability +#: model:ir.model.fields,field_description:purchase_usability.field_product_template__id +#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order__id +#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order_line__id +#: model:ir.model.fields,field_description:purchase_usability.field_res_partner__id +msgid "ID" +msgstr "" + +#. module: purchase_usability +#: model:ir.model.fields,help:purchase_usability.field_purchase_order_line__product_barcode +msgid "International Article Number used for product identification." +msgstr "" + +#. module: purchase_usability +#: model:ir.model.fields,field_description:purchase_usability.field_product_template____last_update +#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order____last_update +#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order_line____last_update +#: model:ir.model.fields,field_description:purchase_usability.field_res_partner____last_update +msgid "Last Modified on" +msgstr "" + +#. module: purchase_usability +#: model:ir.model.fields.selection,name:purchase_usability.selection__purchase_order_line__invoice_status__no +msgid "Nothing to Bill" +msgstr "" + +#. module: purchase_usability +#: model:ir.model.fields,help:purchase_usability.field_product_product__purchase_method +#: model:ir.model.fields,help:purchase_usability.field_product_template__purchase_method +msgid "" +"On ordered quantities: Control bills based on ordered quantities.\n" +"On received quantities: Control bills based on received quantities." +msgstr "" + +#. module: purchase_usability +#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order__payment_term_id +msgid "Payment Terms" +msgstr "" + +#. module: purchase_usability +#: model_terms:ir.ui.view,arch_db:purchase_usability.purchase_order_form +msgid "Print" +msgstr "" + +#. module: purchase_usability +#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order_line__product_barcode +msgid "Product Barcode" +msgstr "" + +#. module: purchase_usability +#: model:ir.model,name:purchase_usability.model_product_template +msgid "Product Template" +msgstr "" + +#. module: purchase_usability +#: model:ir.model,name:purchase_usability.model_purchase_order +#: model:ir.model.fields,field_description:purchase_usability.field_res_partner__purchase_warn +#: model:ir.model.fields,field_description:purchase_usability.field_res_users__purchase_warn +msgid "Purchase Order" +msgstr "" + +#. module: purchase_usability +#: model:ir.model,name:purchase_usability.model_purchase_order_line +msgid "Purchase Order Line" +msgstr "" + +#. module: purchase_usability +#: model:ir.model.fields,field_description:purchase_usability.field_product_product__purchase_line_warn +#: model:ir.model.fields,field_description:purchase_usability.field_product_template__purchase_line_warn +msgid "Purchase Order Line Warning" +msgstr "" + +#. module: purchase_usability +#: model:ir.model.fields,help:purchase_usability.field_purchase_order__dest_address_id +msgid "" +"Put an address if you want to deliver directly from the vendor to the " +"customer. Otherwise, keep empty to deliver to your own company." +msgstr "" + +#. module: purchase_usability +#: model:ir.model.fields,help:purchase_usability.field_purchase_order__partner_ref +msgid "" +"Reference of the sales order or bid sent by the vendor. It's used to do the " +"matching when you receive the products as this reference is usually written " +"on the delivery order sent by your vendor." +msgstr "" + +#. module: purchase_usability +#: model_terms:ir.ui.view,arch_db:purchase_usability.view_purchase_order_filter +msgid "Reference, Origin or Vendor Reference" +msgstr "" + +#. module: purchase_usability +#: model:ir.model.fields,help:purchase_usability.field_product_product__purchase_line_warn +#: model:ir.model.fields,help:purchase_usability.field_product_template__purchase_line_warn +#: model:ir.model.fields,help:purchase_usability.field_res_partner__purchase_warn +#: model:ir.model.fields,help:purchase_usability.field_res_users__purchase_warn +msgid "" +"Selecting the \"Warning\" option will notify user with the message, " +"Selecting \"Blocking Message\" will throw an exception with the message and " +"block the flow. The Message has to be written in the next field." +msgstr "" + +#. module: purchase_usability +#: model:ir.model.fields,field_description:purchase_usability.field_purchase_order__partner_ref +msgid "Vendor Reference" +msgstr "" + +#. module: purchase_usability +#: model:ir.model.fields.selection,name:purchase_usability.selection__purchase_order_line__invoice_status__to_invoice +#: model_terms:ir.ui.view,arch_db:purchase_usability.purchase_order_line_search +msgid "Waiting Bills" +msgstr "" diff --git a/purchase_usability/models/purchase_order.py b/purchase_usability/models/purchase_order.py index 71774384..c373d2e7 100644 --- a/purchase_usability/models/purchase_order.py +++ b/purchase_usability/models/purchase_order.py @@ -4,6 +4,7 @@ from odoo import api, fields, models from odoo.tools.misc import formatLang +from odoo.tools import float_is_zero class PurchaseOrder(models.Model): @@ -73,3 +74,33 @@ class PurchaseOrderLine(models.Model): # for optional display in tree view product_barcode = fields.Char(related='product_id.barcode', string="Product Barcode") + invoice_status = fields.Selection( + [ + ("no", "Nothing to Bill"), + ("to invoice", "Waiting Bills"), + ("invoiced", "Fully Billed"), + ], + string="Billing Status", + compute="_compute_invoice_status", + store=True, + readonly=True, + default="no", + ) + + @api.depends("state", "qty_to_invoice", "qty_invoiced") + def _compute_invoice_status(self): + """Mimic PO '_get_invoiced' method to compute PO line invoice status""" + prec = self.env["decimal.precision"].precision_get("Product Unit of Measure") + for line in self: + if line.state not in ("purchase", "done") or line.display_type: + line.invoice_status = "no" + continue + + if not float_is_zero(line.qty_to_invoice, precision_digits=prec): + line.invoice_status = "to invoice" + elif float_is_zero( + line.qty_to_invoice, precision_digits=prec + ) and not float_is_zero(line.qty_invoiced, precision_digits=prec): + line.invoice_status = "invoiced" + else: + line.invoice_status = "no" diff --git a/purchase_usability/views/purchase_order.xml b/purchase_usability/views/purchase_order.xml index 023e0972..314e9f92 100644 --- a/purchase_usability/views/purchase_order.xml +++ b/purchase_usability/views/purchase_order.xml @@ -131,7 +131,10 @@ - + + @@ -144,7 +147,13 @@ + + + + + + diff --git a/sale_purchase_no_product_template_menu/__init__.py b/sale_purchase_no_product_template_menu/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/sale_purchase_no_product_template_menu/__manifest__.py b/sale_purchase_no_product_template_menu/__manifest__.py deleted file mode 100644 index 79242496..00000000 --- a/sale_purchase_no_product_template_menu/__manifest__.py +++ /dev/null @@ -1,29 +0,0 @@ -# Copyright 2015-2019 Akretion France (http://www.akretion.com/) -# @author: Alexis de Lattre -# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). - -{ - 'name': 'Sale Purchase No Product Template Menu', - 'version': '12.0.1.0.0', - 'category': 'Sale and Purchase', - 'license': 'AGPL-3', - 'summary': "Replace product.template menu entries by product.product menu entries", - 'description': """ -Sale Purchase No Product Template -================================= - -This module replaces the menu entries for product.template by menu entries for product.product in the *Sales* and *Purchases* menu entries. With this module, the only menu entry for product.template is in the menu *Sales > Configuration > Product Categories and Attributes*. - -This module also switches to the tree view by default for Product menu entries, instead of the kanban view. - -This module has been written by Alexis de Lattre from Akretion . - """, - 'author': 'Akretion', - 'website': 'http://www.akretion.com', - 'depends': [ - 'purchase', - 'sale', - ], - 'data': ['view.xml'], - 'installable': False, -} diff --git a/sale_purchase_no_product_template_menu/i18n/fr.po b/sale_purchase_no_product_template_menu/i18n/fr.po deleted file mode 100644 index 9e549dd2..00000000 --- a/sale_purchase_no_product_template_menu/i18n/fr.po +++ /dev/null @@ -1,33 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * sale_purchase_no_product_template_menu -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 8.0\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-05-30 15:27+0000\n" -"PO-Revision-Date: 2016-05-30 15:27+0000\n" -"Last-Translator: <>\n" -"Language-Team: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Plural-Forms: \n" - -#. module: sale_purchase_no_product_template_menu -#: model:ir.ui.menu,name:sale_purchase_no_product_template_menu.sale_config_product_template_menu -msgid "Product Templates" -msgstr "Modèles d'article" - -#. module: sale_purchase_no_product_template_menu -#: model:ir.actions.act_window,name:sale_purchase_no_product_template_menu.product_product_action_puchased -#: model:ir.actions.act_window,name:sale_purchase_no_product_template_menu.product_product_action_sell -msgid "Products" -msgstr "Articles" - -#. module: sale_purchase_no_product_template_menu -#: view:product.product:sale_purchase_no_product_template_menu.product_normal_form_view -msgid "{'invisible': 1, 'required': 0}" -msgstr "{'invisible': 1, 'required': 0}" - diff --git a/sale_purchase_no_product_template_menu/i18n/sale_purchase_no_product_template_menu.pot b/sale_purchase_no_product_template_menu/i18n/sale_purchase_no_product_template_menu.pot deleted file mode 100644 index 2d5822aa..00000000 --- a/sale_purchase_no_product_template_menu/i18n/sale_purchase_no_product_template_menu.pot +++ /dev/null @@ -1,33 +0,0 @@ -# Translation of Odoo Server. -# This file contains the translation of the following modules: -# * sale_purchase_no_product_template_menu -# -msgid "" -msgstr "" -"Project-Id-Version: Odoo Server 8.0\n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2016-05-30 15:27+0000\n" -"PO-Revision-Date: 2016-05-30 15:27+0000\n" -"Last-Translator: <>\n" -"Language-Team: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: \n" -"Plural-Forms: \n" - -#. module: sale_purchase_no_product_template_menu -#: model:ir.ui.menu,name:sale_purchase_no_product_template_menu.sale_config_product_template_menu -msgid "Product Templates" -msgstr "" - -#. module: sale_purchase_no_product_template_menu -#: model:ir.actions.act_window,name:sale_purchase_no_product_template_menu.product_product_action_puchased -#: model:ir.actions.act_window,name:sale_purchase_no_product_template_menu.product_product_action_sell -msgid "Products" -msgstr "" - -#. module: sale_purchase_no_product_template_menu -#: view:product.product:sale_purchase_no_product_template_menu.product_normal_form_view -msgid "{'invisible': 1, 'required': 0}" -msgstr "" - diff --git a/sale_purchase_no_product_template_menu/view.xml b/sale_purchase_no_product_template_menu/view.xml deleted file mode 100644 index 0bf6ca19..00000000 --- a/sale_purchase_no_product_template_menu/view.xml +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - - - Products - product.product - tree,form,kanban - {'search_default_filter_to_purchase': 1} - - - - - - - - - - - - Products - product.product - tree,form,kanban - {'search_default_filter_to_sell': 1} - - - - - - - - - - - - - Product Templates - tree,form,kanban - - {} - - - - - - - - - tree,form,kanban - - - - diff --git a/sales_team_usability/__init__.py b/sales_team_usability/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/sales_team_usability/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/sales_team_usability/__manifest__.py b/sales_team_usability/__manifest__.py new file mode 100644 index 00000000..7f040d3a --- /dev/null +++ b/sales_team_usability/__manifest__.py @@ -0,0 +1,27 @@ +# Copyright 2021 Akretion France (http://www.akretion.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +# @author Alexis de Lattre + +{ + 'name': 'Sales Teams Usability', + 'version': '14.0.1.0.0', + 'category': 'Sales/Sales', + 'license': 'AGPL-3', + 'summary': 'Sales Teams usability enhancements', + 'description': """ +Sales Teams Usability +===================== + +The usability improvements include: + +* set 'name' field of crm.tag un-translatable + +This module has been written by Alexis de Lattre from Akretion +. + """, + 'author': 'Akretion', + 'website': 'http://www.akretion.com', + 'depends': ['sales_team'], + 'data': [], + 'installable': True, +} diff --git a/sales_team_usability/models/__init__.py b/sales_team_usability/models/__init__.py new file mode 100644 index 00000000..cb3133d6 --- /dev/null +++ b/sales_team_usability/models/__init__.py @@ -0,0 +1 @@ +from . import crm_tag diff --git a/sales_team_usability/models/crm_tag.py b/sales_team_usability/models/crm_tag.py new file mode 100644 index 00000000..e813e517 --- /dev/null +++ b/sales_team_usability/models/crm_tag.py @@ -0,0 +1,11 @@ +# Copyright 2021 Akretion France (http://www.akretion.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +# @author Alexis de Lattre + +from odoo import fields, models + + +class CrmTag(models.Model): + _inherit = "crm.tag" + + name = fields.Char(translate=False) diff --git a/stock_reception_usability/__init__.py b/stock_reception_usability/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/stock_reception_usability/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/stock_reception_usability/__manifest__.py b/stock_reception_usability/__manifest__.py new file mode 100644 index 00000000..5cf97737 --- /dev/null +++ b/stock_reception_usability/__manifest__.py @@ -0,0 +1,24 @@ +# Copyright 2021 Akretion (https://www.akretion.com). +# @author Kévin Roche +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Stock relation usability", + "summary": "SUMMARY", + "version": "14.0.1.0.0", + "category": "Inventory, Logistic, Storage", + "website": "http://www.akretion.com", + "author": "Akretion", + "license": "AGPL-3", + "application": False, + "installable": True, + "depends": [ + "stock", + "purchase", + ], + "data": [ + "views/stock_picking.xml", + ], + "demo": [], + "qweb": [], +} diff --git a/stock_reception_usability/models/__init__.py b/stock_reception_usability/models/__init__.py new file mode 100644 index 00000000..a33bde1e --- /dev/null +++ b/stock_reception_usability/models/__init__.py @@ -0,0 +1,2 @@ +from . import stock_move +from . import stock_picking diff --git a/stock_reception_usability/models/stock_move.py b/stock_reception_usability/models/stock_move.py new file mode 100644 index 00000000..bba54081 --- /dev/null +++ b/stock_reception_usability/models/stock_move.py @@ -0,0 +1,36 @@ +# Copyright (C) 2021 Akretion (). +# @author Kévin Roche +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class StockMove(models.Model): + _inherit = "stock.move" + + location_dest_list = fields.Text( + string="Locations", compute="_compute_locations_dest_list" + ) + + @api.depends( + "move_line_ids", "move_line_ids.location_dest_id", "move_line_ids.qty_done" + ) + def _compute_locations_dest_list(self): + for move in self: + data = [] + separator = ", " + dest_list = move.move_line_ids.location_dest_id + for dest in dest_list: + lines_qty = move.move_line_ids.search( + [("move_id", "=", move.id), ("location_dest_id", "=", dest.id)] + ).mapped("qty_done") + quantity = int(sum(lines_qty)) + location = dest.name + data.append("{}: {}".format(quantity, location)) + move.location_dest_list = separator.join(data) + + def _compute_is_quantity_done_editable(self): + super()._compute_is_quantity_done_editable() + for move in self: + if len(move.move_line_ids) == 1 and move.show_details_visible: + move.is_quantity_done_editable = True diff --git a/stock_reception_usability/models/stock_picking.py b/stock_reception_usability/models/stock_picking.py new file mode 100644 index 00000000..cd6b75ca --- /dev/null +++ b/stock_reception_usability/models/stock_picking.py @@ -0,0 +1,38 @@ +# Copyright (C) 2021 Akretion (). +# @author Kévin Roche +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class StockPicking(models.Model): + _inherit = "stock.picking" + + def action_fill_quantity_done(self): + self.ensure_one() + for move in self.move_ids_without_package: + if move.move_line_ids: + first_line = move.move_line_ids[0] + else: + first_line = False + if move.quantity_done == 0 and first_line: + qty = move.product_uom_qty + if first_line.qty_done == 0: + first_line.write( + { + "qty_done": qty, + } + ) + elif move.quantity_done < move.product_uom_qty or ( + move.quantity_done == 0 and not first_line + ): + qty = move.product_uom_qty - move.quantity_done + self.env["stock.move.line"].create( + { + "move_id": move.id, + "location_dest_id": move.location_dest_id.id, + "location_id": move.location_id.id, + "product_uom_id": move.product_uom.id, + "qty_done": qty, + } + ) diff --git a/stock_reception_usability/views/stock_picking.xml b/stock_reception_usability/views/stock_picking.xml new file mode 100644 index 00000000..c59ee622 --- /dev/null +++ b/stock_reception_usability/views/stock_picking.xml @@ -0,0 +1,27 @@ + + + + stock.picking + + + + + + + {'column_invisible': [('parent.state', '=', 'done')]} + + + + + + + + + diff --git a/stock_usability/views/stock_picking.xml b/stock_usability/views/stock_picking.xml index abbfcda3..ac9a654c 100644 --- a/stock_usability/views/stock_picking.xml +++ b/stock_usability/views/stock_picking.xml @@ -34,8 +34,8 @@ - - + + - + stock_usability.view_picking_search stock.picking 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 1593277e..57718856 100644 --- a/stock_valuation_xlsx/__manifest__.py +++ b/stock_valuation_xlsx/__manifest__.py @@ -1,11 +1,11 @@ -# Copyright 2020 Akretion France (http://www.akretion.com) +# Copyright 2020-2021 Akretion France (http://www.akretion.com) # @author Alexis de Lattre # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). { 'name': 'Stock Valuation XLSX', - 'version': '12.0.1.0.0', + 'version': '14.0.1.0.0', 'category': 'Tools', 'license': 'AGPL-3', 'summary': 'Generate XLSX reports for past or present stock levels', @@ -37,8 +37,11 @@ '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': False, + 'installable': True, } diff --git a/stock_valuation_xlsx/models/__init__.py b/stock_valuation_xlsx/models/__init__.py new file mode 100644 index 00000000..eb4d7693 --- /dev/null +++ b/stock_valuation_xlsx/models/__init__.py @@ -0,0 +1 @@ +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..8a961e9c --- /dev/null +++ b/stock_valuation_xlsx/models/stock_expiry_depreciation_rule.py @@ -0,0 +1,35 @@ +# 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.company) + 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..5bea9e25 --- /dev/null +++ b/stock_valuation_xlsx/security/ir.model.access.csv @@ -0,0 +1,5 @@ +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 +access_stock_valuation_xlsx,stock.valuation.xlsx wizard,model_stock_valuation_xlsx,stock.group_stock_user,1,1,1,0 +access_stock_variation_xlsx,stock.variation.xlsx wizard,model_stock_variation_xlsx,stock.group_stock_user,1,1,1,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/views/stock_inventory.xml b/stock_valuation_xlsx/views/stock_inventory.xml index 801c50ff..82228b13 100644 --- a/stock_valuation_xlsx/views/stock_inventory.xml +++ b/stock_valuation_xlsx/views/stock_inventory.xml @@ -16,7 +16,7 @@ + context="{'default_source': 'inventory', 'default_inventory_id': active_id}"/> 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..131d134b 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 @@ -16,59 +17,61 @@ class StockValuationXlsx(models.TransientModel): _name = 'stock.valuation.xlsx' + _check_company_auto = True _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 - # a lot of confusion for users - state = fields.Selection([ - ('setup', 'Setup'), - ('done', 'Done'), - ], string='State', default='setup', readonly=True) + company_id = fields.Many2one( + 'res.company', string='Company', default=lambda self: self.env.company, + required=True) warehouse_id = fields.Many2one( - 'stock.warehouse', string='Warehouse', - states={'done': [('readonly', True)]}) + 'stock.warehouse', string='Warehouse', check_company=True, + domain="[('company_id', '=', company_id)]") 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)]}, + domain="[('usage', 'in', ('view', 'internal')), ('company_id', '=', company_id)]", + default=lambda self: self._default_location(), check_company=True, help="The childen locations of the selected locations will " - u"be taken in the valuation.") + "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.", + ) source = fields.Selection([ ('inventory', 'Physical Inventory'), ('stock', 'Stock Levels'), - ], string='Source data', default='stock', required=True, - states={'done': [('readonly', True)]}) + ], string='Source data', default='stock', required=True) inventory_id = fields.Many2one( - 'stock.inventory', string='Inventory', domain=[('state', '=', 'done')], - states={'done': [('readonly', True)]}) + 'stock.inventory', string='Inventory', check_company=True, + domain="[('state', '=', 'done'), ('company_id', '=', company_id)]") stock_date_type = fields.Selection([ ('present', 'Present'), ('past', 'Past'), - ], string='Present or Past', default='present', - states={'done': [('readonly', True)]}) + ], string='Present or Past', default='present') past_date = fields.Datetime( - string='Past Date', states={'done': [('readonly', True)]}, - default=fields.Datetime.now) + string='Past Date', default=fields.Datetime.now) categ_subtotal = fields.Boolean( string='Subtotals per Categories', default=True, - states={'done': [('readonly', True)]}, - help="Show a subtotal per product category") + help="Show a subtotal per product category.") standard_price_date = fields.Selection([ ('past', 'Past Date or Inventory Date'), ('present', 'Current'), - ], default='past', string='Cost Price Date', - states={'done': [('readonly', True)]}) - 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)]}) + ], default='past', string='Cost Price Date') + 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) + split_by_lot = fields.Boolean(string='Display Lots') + split_by_location = fields.Boolean(string='Display Stock Locations') + + @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): @@ -123,28 +126,41 @@ 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 = fields.Date.to_date(past_date) # convert datetime to 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() 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_past_date: + # if not standard_price_past_date: # TODO + if True: fields_list.append('standard_price') products = ppo.search_read([('id', 'in', in_stock_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_past_date: - history = ppho.search_read([ - ('company_id', '=', company_id), - ('product_id', '=', p['id']), - ('datetime', '<=', standard_price_past_date)], - ['cost'], order='datetime desc, id desc', limit=1) - standard_price = history and history[0]['cost'] or 0.0 + # No more product.price.history on v14 + # We are supposed to use stock.valuation.layer.revaluation + # TODO migrate to stock.valuation.layer.revaluation + #history = ppho.search_read([ + # ('company_id', '=', company_id), + # ('product_id', '=', p['id']), + # ('datetime', '<=', standard_price_past_date)], + # ['cost'], order='datetime desc, id desc', limit=1) + #standard_price = history and history[0]['cost'] or 0.0 + standard_price = p['standard_price'] # TODO remove this tmp stuff else: standard_price = p['standard_price'] product_id2data[p['id']] = {'standard_price': standard_price} @@ -156,38 +172,56 @@ 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_id2data(self, product_ids, has_expiry_date, depreciation_rules): + splo = self.env['stock.production.lot'] lot_id2data = {} lot_fields = ['name'] - if hasattr(splo, 'expiry_date'): + if has_expiry_date: lot_fields.append('expiry_date') lots = splo.search_read( [('product_id', 'in', product_ids)], lot_fields) for lot in lots: lot_id2data[lot['id']] = lot + lot_id2data[lot['id']]['depreciation_ratio'] = 0 + if depreciation_rules and lot.get('expiry_date'): + expiry_date = lot['expiry_date'] + for rule in depreciation_rules: + if expiry_date <= rule['start_date']: + lot_id2data[lot['id']]['depreciation_ratio'] = rule['ratio'] / 100.0 + break + return lot_id2data + + @api.model + def stock_location_id2name(self, location): + slo = self.env['stock.location'].with_context(active_test=False) loc_id2name = {} locs = slo.search_read( [('id', 'child_of', self.location_id.id)], ['display_name']) for loc in locs: loc_id2name[loc['id']] = loc['display_name'] - logger.debug('End id2name') - return categ_id2name, uom_id2name, lot_id2data, loc_id2name + return loc_id2name def compute_data_from_inventory(self, product_ids, prec_qty): self.ensure_one() @@ -275,7 +309,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: @@ -284,17 +318,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']], )) @@ -305,14 +349,19 @@ def stringify_and_sort_result( def generate(self): self.ensure_one() logger.debug('Start generate XLSX stock valuation report') - splo = self.env['stock.production.lot'].with_context(active_test=False) 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 = self.company_id company_id = company.id prec_cur_rounding = company.currency_id.rounding self._check_config(company_id) + apply_depreciation = self.apply_depreciation + if ( + (self.source == 'stock' and self.stock_date_type == 'past') or + not self.split_by_lot or + not self.has_expiry_date): + apply_depreciation = False product_ids = self.get_product_ids() if not product_ids: raise UserError(_("There are no products to analyse.")) @@ -335,15 +384,25 @@ 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, standard_price_past_date=standard_price_past_date) data_res = self.group_result(data, split_by_lot, split_by_location) - categ_id2name, uom_id2name, lot_id2data, loc_id2name = self.id2name(product_ids) + categ_id2name = self.product_categ_id2name(self.categ_ids) + uom_id2name = self.uom_id2name() + lot_id2data = self.prodlot_id2data(in_stock_product_ids, self.has_expiry_date, depreciation_rules) + loc_id2name = self.stock_location_id2name(self.location_id) res = self.stringify_and_sort_result( product_ids, product_id2data, data_res, prec_qty, prec_price, prec_cur_rounding, - categ_id2name, uom_id2name, lot_id2data, loc_id2name) + categ_id2name, uom_id2name, lot_id2data, loc_id2name, apply_depreciation) logger.debug('Start create XLSX workbook') file_data = BytesIO() @@ -356,12 +415,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']): @@ -417,6 +479,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: @@ -432,12 +497,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: @@ -460,21 +533,17 @@ def generate(self): filename = 'Odoo_stock_%s.xlsx' % stock_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 = { - # 'name': _('Stock Valuation XLSX'), - # 'type': 'ir.actions.act_url', - # 'url': "web/content/?model=%s&id=%d&filename_field=export_filename&" - # "field=export_file&download=true&filename=%s" % ( - # self._name, self.id, self.export_filename), - # 'target': 'self', - # } - action = self.env['ir.actions.act_window'].for_xml_id( - 'stock_valuation_xlsx', 'stock_valuation_xlsx_action') - action['res_id'] = self.id + action = { + 'name': _('Stock Valuation XLSX'), + 'type': 'ir.actions.act_url', + 'url': "web/content/?model=%s&id=%d&filename_field=export_filename&" + "field=export_file&download=true&filename=%s" % ( + self._name, self.id, self.export_filename), + 'target': 'new', + } return action def _prepare_styles(self, workbook, company, prec_price): @@ -482,8 +551,8 @@ def _prepare_styles(self, workbook, company, prec_price): categ_bg_color = '#e1daf5' col_title_bg_color = '#fff9b4' regular_font_size = 10 - currency_num_format = u'# ### ##0.00 %s' % company.currency_id.symbol - price_currency_num_format = u'# ### ##0.%s %s' % ('0' * prec_price, company.currency_id.symbol) + currency_num_format = '# ### ##0.00 %s' % company.currency_id.symbol + price_currency_num_format = '# ### ##0.%s %s' % ('0' * prec_price, company.currency_id.symbol) styles = { 'doc_title': workbook.add_format({ 'bold': True, 'font_size': regular_font_size + 10, @@ -503,6 +572,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': '0.%'}), 'regular': workbook.add_format({}), 'regular_small': workbook.add_format({'font_size': regular_font_size - 2}), 'categ_title': workbook.add_format({ @@ -527,8 +597,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 16a35fb1..7d9cf4cf 100644 --- a/stock_valuation_xlsx/wizard/stock_valuation_xlsx_view.xml +++ b/stock_valuation_xlsx/wizard/stock_valuation_xlsx_view.xml @@ -17,7 +17,7 @@ The generated XLSX report has the valuation of stockable products located on the selected stock locations (and their childrens). - + @@ -27,18 +27,14 @@ + - - - - + @@ -55,6 +51,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..b29cd85b --- /dev/null +++ b/stock_valuation_xlsx/wizard/stock_variation_xlsx.py @@ -0,0 +1,458 @@ +# 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' + _check_company_auto = True + _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) + company_id = fields.Many2one( + 'res.company', string='Company', default=lambda self: self.env.company, + required=True) + warehouse_id = fields.Many2one( + 'stock.warehouse', string='Warehouse', check_company=True, + domain="[('company_id', '=', company_id)]") + location_id = fields.Many2one( + 'stock.location', string='Root Stock Location', required=True, + domain="[('usage', 'in', ('view', 'internal')), ('company_id', '=', company_id)]", + default=lambda self: self._default_location(), check_company=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.") + start_date = fields.Datetime( + string='Start Date', required=True) + standard_price_start_date_type = fields.Selection([ + ('start', 'Start Date'), + ('present', 'Current'), + ], default='start', required=True, + string='Cost Price for Start Date') + end_date_type = fields.Selection([ + ('present', 'Present'), + ('past', 'Past'), + ], string='End Date Type', default='present', required=True) + end_date = fields.Datetime( + string='End Date', 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) + categ_subtotal = fields.Boolean( + string='Subtotals per Categories', default=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'] + fields_list = self._prepare_product_fields() + # if not standard_price_start_date or not standard_price_end_date: # TODO + if True: + 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']) + if standard_price_start_date: + # No more product.price.history on v14 + # We are supposed to use stock.valuation.layer.revaluation + # TODO migrate to stock.valuation.layer.revaluation + #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 + start_standard_price = p['standard_price'] # TODO remove this tmp stuff + 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 + end_standard_price = p['standard_price'] # TODO remove this tmp stuff + 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).with_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.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({ + 'export_filename': filename, + 'export_file': export_file_b64, + }) + action = { + 'name': _('Stock Variation XLSX'), + 'type': 'ir.actions.act_url', + 'url': "web/content/?model=%s&id=%d&filename_field=export_filename&" + "field=export_file&download=true&filename=%s" % ( + self._name, self.id, self.export_filename), + 'target': 'new', + } + 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..9ac60aa4 --- /dev/null +++ b/stock_valuation_xlsx/wizard/stock_variation_xlsx_view.xml @@ -0,0 +1,55 @@ + + + + + + + + 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 + + + + + +
The generated XLSX report has the valuation of stockable products located on the selected stock locations (and their childrens).