From a707920c2eb882f0592bfb00f9656ecbf06f6bf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20BEAU?= Date: Fri, 9 Dec 2022 00:03:15 +0100 Subject: [PATCH 1/6] product_pricelist_per_attribute_value: fix domain on attribute value --- .../__manifest__.py | 5 +- .../models/product_pricelist.py | 55 ++++++++++++++----- .../views/product_pricelist.xml | 2 + 3 files changed, 46 insertions(+), 16 deletions(-) diff --git a/product_pricelist_per_attribute_value/__manifest__.py b/product_pricelist_per_attribute_value/__manifest__.py index 7d70fd201..ace32acd8 100644 --- a/product_pricelist_per_attribute_value/__manifest__.py +++ b/product_pricelist_per_attribute_value/__manifest__.py @@ -8,7 +8,10 @@ "author": "Akretion, Odoo Community Association (OCA)", "website": "https://github.com/akretion/ak-odoo-incubator", "license": "AGPL-3", - "depends": ["product"], + "depends": [ + "product", + "web_domain_field", + ], "data": [ "views/product_pricelist.xml", ], diff --git a/product_pricelist_per_attribute_value/models/product_pricelist.py b/product_pricelist_per_attribute_value/models/product_pricelist.py index e910e6e6f..cd825052c 100644 --- a/product_pricelist_per_attribute_value/models/product_pricelist.py +++ b/product_pricelist_per_attribute_value/models/product_pricelist.py @@ -2,6 +2,7 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). import itertools +import json from odoo import api, fields, models @@ -34,6 +35,12 @@ class PricelistItem(models.Model): string="Attribute Values", help="Specify values if this rule only applies to this product " "attribute values. Keep empty otherwise.", + compute="_compute_product_attribute_value_ids", + readonly=False, + store=True, + ) + product_attribute_value_domain = fields.Char( + compute="_compute_product_attribute_value_domain", ) @api.depends("product_attribute_value_ids") @@ -41,21 +48,39 @@ def _compute_attribute_value_restricted(self): for record in self: record.attribute_value_restricted = bool(record.product_attribute_value_ids) - @api.onchange("applied_on", "product_tmpl_id", "categ_id") - def _onchange_attribute_value_domain(self): - self.ensure_one() - domain = [] - self.product_attribute_value_ids = None - if self.applied_on == "1_product" and self.product_tmpl_id: - values = self.product_tmpl_id.attribute_line_ids.value_ids - domain = [("id", "in", values.ids)] - elif self.applied_on == "2_product_category" and self.categ_id: - product_templates = self.env["product.template"].search( - [("categ_id", "child_of", self.categ_id.id)] - ) - values = product_templates.attribute_line_ids.value_ids - domain = [("id", "in", values.ids)] - return {"domain": {"product_attribute_value_ids": domain}} + @api.depends("applied_on", "product_tmpl_id", "categ_id") + def _compute_product_attribute_value_domain(self): + for record in self: + if record.applied_on == "1_product" and record.product_tmpl_id: + domain = [ + ( + "id", + "in", + record.product_tmpl_id.attribute_line_ids.value_ids.ids, + ) + ] + elif record.applied_on == "2_product_category" and record.categ_id: + product_templates = record.env["product.template"].search( + [("categ_id", "child_of", record.categ_id.id)] + ) + domain = [ + ("id", "in", product_templates.attribute_line_ids.value_ids.ids) + ] + else: + domain = [] + record.product_attribute_value_domain = json.dumps(domain) + + @api.depends("applied_on", "product_tmpl_id", "categ_id") + def _compute_product_attribute_value_ids(self): + for record in self: + if record.applied_on == "0_product_variant": + record.product_attribute_value_ids = None + elif record.product_attribute_value_ids: + record.product_attribute_value_ids = ( + record.product_attribute_value_ids.filtered_domain( + json.loads(record.product_attribute_value_domain) + ) + ) @api.depends("product_attribute_value_ids.name") def _get_pricelist_item_name_price(self): diff --git a/product_pricelist_per_attribute_value/views/product_pricelist.xml b/product_pricelist_per_attribute_value/views/product_pricelist.xml index 851513b92..d75b6f954 100644 --- a/product_pricelist_per_attribute_value/views/product_pricelist.xml +++ b/product_pricelist_per_attribute_value/views/product_pricelist.xml @@ -10,8 +10,10 @@ name="pricelist_rule_target_attribute_value" attrs="{'invisible':[('applied_on', '=', '0_product_variant')]}" > + From 8a05c65a74e1d6faec4d147cb80c42e0e236fa26 Mon Sep 17 00:00:00 2001 From: Kev-Roche Date: Sun, 8 Jan 2023 21:11:22 +0100 Subject: [PATCH 2/6] [ADD] purchase_requisition_proposal module --- purchase_requisition_proposal/__init__.py | 2 + purchase_requisition_proposal/__manifest__.py | 33 + purchase_requisition_proposal/data/data.xml | 107 ++++ purchase_requisition_proposal/i18n/fr.po | 596 ++++++++++++++++++ .../models/__init__.py | 9 + .../models/ir_config.py | 16 + .../models/purchase_order.py | 25 + .../models/purchase_requisition.py | 171 +++++ .../purchase_requisition_information_mixin.py | 51 ++ .../models/purchase_requisition_line.py | 109 ++++ .../models/purchase_requisition_proposal.py | 101 +++ .../models/purchase_requisition_type.py | 51 ++ .../models/res_company.py | 17 + .../models/sale_order.py | 32 + .../readme/CONTRIBUTORS.rst | 1 + .../readme/DESCRIPTION.rst | 1 + .../security/ir.model.access.csv | 3 + .../purchase_requisition_security.xml | 35 + .../static/description/icon.png | Bin 0 -> 25842 bytes .../tests/__init__.py | 1 + .../test_purchase_requirement_proposal.py | 182 ++++++ .../views/ir_config.xml | 28 + .../views/purchase_order.xml | 20 + .../views/purchase_requisition.xml | 219 +++++++ .../views/purchase_requisition_proposal.xml | 57 ++ .../views/purchase_requisition_type.xml | 43 ++ .../views/res_config.xml | 34 + .../views/sale_order.xml | 22 + .../wizard/__init__.py | 1 + .../wizard/wizard_requisition_proposal.py | 31 + .../wizard/wizard_requisition_proposal.xml | 47 ++ .../odoo/addons/purchase_requisition_proposal | 1 + setup/purchase_requisition_proposal/setup.py | 6 + 33 files changed, 2052 insertions(+) create mode 100644 purchase_requisition_proposal/__init__.py create mode 100644 purchase_requisition_proposal/__manifest__.py create mode 100644 purchase_requisition_proposal/data/data.xml create mode 100644 purchase_requisition_proposal/i18n/fr.po create mode 100644 purchase_requisition_proposal/models/__init__.py create mode 100644 purchase_requisition_proposal/models/ir_config.py create mode 100644 purchase_requisition_proposal/models/purchase_order.py create mode 100644 purchase_requisition_proposal/models/purchase_requisition.py create mode 100644 purchase_requisition_proposal/models/purchase_requisition_information_mixin.py create mode 100644 purchase_requisition_proposal/models/purchase_requisition_line.py create mode 100644 purchase_requisition_proposal/models/purchase_requisition_proposal.py create mode 100644 purchase_requisition_proposal/models/purchase_requisition_type.py create mode 100644 purchase_requisition_proposal/models/res_company.py create mode 100644 purchase_requisition_proposal/models/sale_order.py create mode 100644 purchase_requisition_proposal/readme/CONTRIBUTORS.rst create mode 100644 purchase_requisition_proposal/readme/DESCRIPTION.rst create mode 100644 purchase_requisition_proposal/security/ir.model.access.csv create mode 100644 purchase_requisition_proposal/security/purchase_requisition_security.xml create mode 100644 purchase_requisition_proposal/static/description/icon.png create mode 100644 purchase_requisition_proposal/tests/__init__.py create mode 100644 purchase_requisition_proposal/tests/test_purchase_requirement_proposal.py create mode 100644 purchase_requisition_proposal/views/ir_config.xml create mode 100644 purchase_requisition_proposal/views/purchase_order.xml create mode 100644 purchase_requisition_proposal/views/purchase_requisition.xml create mode 100644 purchase_requisition_proposal/views/purchase_requisition_proposal.xml create mode 100644 purchase_requisition_proposal/views/purchase_requisition_type.xml create mode 100644 purchase_requisition_proposal/views/res_config.xml create mode 100644 purchase_requisition_proposal/views/sale_order.xml create mode 100644 purchase_requisition_proposal/wizard/__init__.py create mode 100644 purchase_requisition_proposal/wizard/wizard_requisition_proposal.py create mode 100644 purchase_requisition_proposal/wizard/wizard_requisition_proposal.xml create mode 120000 setup/purchase_requisition_proposal/odoo/addons/purchase_requisition_proposal create mode 100644 setup/purchase_requisition_proposal/setup.py diff --git a/purchase_requisition_proposal/__init__.py b/purchase_requisition_proposal/__init__.py new file mode 100644 index 000000000..9b4296142 --- /dev/null +++ b/purchase_requisition_proposal/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/purchase_requisition_proposal/__manifest__.py b/purchase_requisition_proposal/__manifest__.py new file mode 100644 index 000000000..d737c00a2 --- /dev/null +++ b/purchase_requisition_proposal/__manifest__.py @@ -0,0 +1,33 @@ +# Copyright 2023 Akretion (https://www.akretion.com). +# @author Kévin Roche +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Purchase Requirement Proposal", + "summary": "SUMMARY", + "version": "14.0.1.0.0", + "category": "CAT", + "website": "https://github.com/akretion/ak-odoo-incubator", + "author": "Akretion, Odoo Community Association (OCA)", + "maintainers": ["Kev-Roche"], + "license": "AGPL-3", + "application": False, + "installable": True, + "depends": [ + "purchase_requisition", + "purchase_sale_inter_company", + "mail", + ], + "data": [ + "data/data.xml", + "views/purchase_order.xml", + "views/sale_order.xml", + "views/purchase_requisition.xml", + "views/purchase_requisition_proposal.xml", + "views/purchase_requisition_type.xml", + "wizard/wizard_requisition_proposal.xml", + "views/ir_config.xml", + "security/ir.model.access.csv", + "security/purchase_requisition_security.xml", + ], +} diff --git a/purchase_requisition_proposal/data/data.xml b/purchase_requisition_proposal/data/data.xml new file mode 100644 index 000000000..d4ac48d5b --- /dev/null +++ b/purchase_requisition_proposal/data/data.xml @@ -0,0 +1,107 @@ + + + + + Intercompany Call for Proposals + -1 + copy + none + proposals + + + Purchase Requisition Proposal + purchase.requisition.proposal + PROP + 5 + + + + Purchase Requisition Call + purchase.requisition.purchase.call + CALL + 5 + + + + Requistion Call: Send by email + + ${object.company_id.name} Call for Proposals (Ref ${object.name or 'n/a' }) + ${','.join([str(partner.id) for partner in object.company_to_call_ids.partner_id]} + +
+

+ Dear collaborator,

+ ${object.company_id.name} have a new Call for Proposals: ${object.name}.
+ The Agreement Deadline is ${format_date(object.date_end)}. +

+ Do not hesitate to contact us if you have any questions. +

+

+ + + + + + + + + +
ProductsSchedule DateQuantityPrice
+ % for line in object.line_ids: + + + + + + + + + +
${line.product_id.display_name}${format_date(line.schedule_date)} ${line.product_qty} ${line.product_uom_id.name} + ${line.price_unit} +
+ % endfor +

+ Best regards, + % if user.signature: +
+ ${user.signature | safe} + % endif +
+

+
+
+ ${(object.name or '').replace('/','-')} + ${object.company_id.lang} + +
+
diff --git a/purchase_requisition_proposal/i18n/fr.po b/purchase_requisition_proposal/i18n/fr.po new file mode 100644 index 000000000..5f1848a8f --- /dev/null +++ b/purchase_requisition_proposal/i18n/fr.po @@ -0,0 +1,596 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * purchase_requisition_proposal +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-01-16 22:50+0000\n" +"PO-Revision-Date: 2023-01-16 23:55+0100\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: \n" +"X-Generator: Poedit 3.2.2\n" + +#. module: purchase_requisition_proposal +#: model:mail.template,report_name:purchase_requisition_proposal.mail_template_requisition_proposal +msgid "${(object.name or '').replace('/','-')}" +msgstr "" + +#. module: purchase_requisition_proposal +#: model:mail.template,subject:purchase_requisition_proposal.mail_template_requisition_proposal +msgid "${object.company_id.name} Call for Proposals (Ref ${object.name or 'n/a' })" +msgstr "${object.company_id.name} Appel d'Offres (Ref ${object.name or 'n/a' })" + +#. module: purchase_requisition_proposal +#: model:mail.template,body_html:purchase_requisition_proposal.mail_template_requisition_proposal +msgid "" +"\n" +"
\n" +"

\n" +" Dear collaborator,

\n" +" ${object.company_id.name} have a new Call for Proposals: ${object.name}.
\n" +" The Agreement Deadline is ${format_date(object.date_end)}.\n" +"

\n" +" Do not hesitate to contact us if you have any questions.\n" +"

\n" +"

\n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +"
ProductsSchedule DateQuantityPrice
\n" +" % for line in object.line_ids:\n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +"
${line.product_id.display_name}${format_date(line.schedule_date)} ${line.product_qty} ${line.product_uom_id.name}\n" +" ${line.price_unit}\n" +"
\n" +" % endfor\n" +"

\n" +" Best regards,\n" +" % if user.signature:\n" +"
\n" +" ${user.signature | safe}\n" +" % endif\n" +"
\n" +"

\n" +"
\n" +" " +msgstr "" +"\n" +"
\n" +"

\n" +" Cher collaborateur,

\n" +" {object.company_id.name} $ a un nouvel appel d'offres : ${object.name}.
\n" +" La date limite de l'accord est ${format_date(object.date_end)}.\n" +"

\n" +" N'hésitez pas à nous contacter si vous avez des questions.\n" +"

\n" +"

\n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +"
ProduitsDate de LivraisonQuantitéPrix
\n" +" % for line in object.line_ids :\n" +" \n" +" \n" +" \n" +"
$
\n" +"
${line.product_id." +"display_name}\n" +" ${format_date(line.schedule_date)} ${line.product_qty} $
\n" +" \n" +" \n" +" % endfor\n" +"
{line.product_uom_id.name}\n" +" \n" +" ${line.price_unit}\n" +"
\n" +"

\n" +" Cordialement\n" +" % if user.signature :\n" +"
\n" +" ${user.signature | safe}\n" +" % endif\n" +"
\n" +"

\n" +"
\n" +" " + +#. module: purchase_requisition_proposal +#: model_terms:ir.ui.view,arch_db:purchase_requisition_proposal.view_purchase_requisition_no_edit_form +msgid "Accept" +msgstr "Accepter" + +#. module: purchase_requisition_proposal +#: model_terms:ir.ui.view,arch_db:purchase_requisition_proposal.view_purchase_requisition_no_edit_form +msgid "Accept and Modify" +msgstr "Accepter et Modifier" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition__exclusive +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_type__exclusive +msgid "Agreement Selection Type" +msgstr "Type de sélection du contrat" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields.selection,name:purchase_requisition_proposal.selection__purchase_requisition_type__multi_company_selection__all +msgid "All Companies" +msgstr "Toutes les sociétés" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields.selection,name:purchase_requisition_proposal.selection__purchase_requisition_type__exclusive__proposals +msgid "Call for Proposals (non-exclusive)" +msgstr "Appel d'Offres multi-sociétés" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_proposal__product_uom_category_id +msgid "Category" +msgstr "" + +#. module: purchase_requisition_proposal +#: model:ir.model,name:purchase_requisition_proposal.model_res_company +msgid "Companies" +msgstr "Sociétés" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition__company_to_call_ids +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_type__company_to_call_ids +msgid "Companies To Call" +msgstr "Sociétés Appelées" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_proposal__company_id +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_proposal__requisition_company_id +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_type__company_id +msgid "Company" +msgstr "Société" + +#. module: purchase_requisition_proposal +#: model:ir.model,name:purchase_requisition_proposal.model_res_config_settings +msgid "Config Settings" +msgstr "Paramètres de config" + +#. module: purchase_requisition_proposal +#: model_terms:ir.ui.view,arch_db:purchase_requisition_proposal.requisition_proposal_wizard +msgid "Confirm" +msgstr "" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,help:purchase_requisition_proposal.field_purchase_requisition_proposal__product_uom_category_id +msgid "Conversion between Units of Measure can only occur if they belong to the same category. The conversion will be made based on the ratios." +msgstr "" + +#. module: purchase_requisition_proposal +#: model_terms:ir.ui.view,arch_db:purchase_requisition_proposal.purchase_requisition_view_form +msgid "Create proposal Quotations" +msgstr "Créer Commandes d'Achat" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_proposal__create_uid +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_requisition_proposal_wizard__create_uid +msgid "Created by" +msgstr "" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_proposal__create_date +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_requisition_proposal_wizard__create_date +msgid "Created on" +msgstr "" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_information_mixin__date_check +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_line__date_check +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_proposal__date_check +msgid "Date Check" +msgstr "Vérif. date" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition__date_end +msgid "Date End" +msgstr "Date Limite" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition__schedule_date +msgid "Delivery Date" +msgstr "Date de livraison" + +#. module: purchase_requisition_proposal +#: model_terms:ir.ui.view,arch_db:purchase_requisition_proposal.requisition_proposal_wizard +msgid "Discard" +msgstr "" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_order__display_name +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_order_line__display_name +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition__display_name +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_information_mixin__display_name +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_line__display_name +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_proposal__display_name +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_type__display_name +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_requisition_proposal_wizard__display_name +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_res_company__display_name +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_res_config_settings__display_name +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_sale_order__display_name +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_sale_order_line__display_name +msgid "Display Name" +msgstr "Nom" + +#. module: purchase_requisition_proposal +#: model_terms:ir.ui.view,arch_db:purchase_requisition_proposal.received_requisition_proposal_line_view_tree +#: model_terms:ir.ui.view,arch_db:purchase_requisition_proposal.requisition_proposal_buttons_wizard +msgid "Duplicate" +msgstr "Dupliquer" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields.selection,name:purchase_requisition_proposal.selection__purchase_requisition_information_mixin__qty_check__enough +#: model:ir.model.fields.selection,name:purchase_requisition_proposal.selection__purchase_requisition_line__qty_check__enough +#: model:ir.model.fields.selection,name:purchase_requisition_proposal.selection__purchase_requisition_proposal__qty_check__enough +msgid "Enough" +msgstr "Suffisant" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_sale_order_line__requirement_proposal_id +msgid "From Proposal" +msgstr "Offre de l'appel" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_order__id +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_order_line__id +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition__id +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_information_mixin__id +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_line__id +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_proposal__id +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_type__id +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_requisition_proposal_wizard__id +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_res_company__id +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_res_config_settings__id +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_sale_order__id +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_sale_order_line__id +msgid "ID" +msgstr "" + +#. module: purchase_requisition_proposal +#: model:purchase.requisition.type,name:purchase_requisition_proposal.call_for_proposals_type +msgid "Intercompany Call for Proposals" +msgstr "Appel d'offre multi-sociétés" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_line__is_proposal_line_validate +msgid "Is Line Validate" +msgstr "" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_order____last_update +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_order_line____last_update +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition____last_update +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_information_mixin____last_update +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_line____last_update +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_proposal____last_update +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_type____last_update +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_requisition_proposal_wizard____last_update +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_res_company____last_update +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_res_config_settings____last_update +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_sale_order____last_update +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_sale_order_line____last_update +msgid "Last Modified on" +msgstr "Dernière modification le" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_proposal__write_uid +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_requisition_proposal_wizard__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_proposal__write_date +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_requisition_proposal_wizard__write_date +msgid "Last Updated on" +msgstr "" + +#. module: purchase_requisition_proposal +#: model_terms:ir.ui.view,arch_db:purchase_requisition_proposal.view_purchase_requisition_type_form_inherit +msgid "Multi Companies" +msgstr "Multi-sociétés" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_type__multi_company_selection +msgid "Multi Company Selection" +msgstr "Sélectionner les sociétés" + +#. module: purchase_requisition_proposal +#: model:ir.ui.menu,name:purchase_requisition_proposal.menu_my_purchase_requisition +msgid "My Requisitions" +msgstr "Mes Appels d'offre" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_proposal__name +msgid "Name" +msgstr "" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields.selection,name:purchase_requisition_proposal.selection__purchase_requisition_type__multi_company_selection__no +msgid "No" +msgstr "Non" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields.selection,name:purchase_requisition_proposal.selection__purchase_requisition_information_mixin__qty_check__not_enough +#: model:ir.model.fields.selection,name:purchase_requisition_proposal.selection__purchase_requisition_line__qty_check__not_enough +#: model:ir.model.fields.selection,name:purchase_requisition_proposal.selection__purchase_requisition_proposal__qty_check__not_enough +msgid "Not Enough" +msgstr "Non Suffisant" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields.selection,name:purchase_requisition_proposal.selection__purchase_requisition_information_mixin__date_check__not_respected +#: model:ir.model.fields.selection,name:purchase_requisition_proposal.selection__purchase_requisition_line__date_check__not_respected +#: model:ir.model.fields.selection,name:purchase_requisition_proposal.selection__purchase_requisition_proposal__date_check__not_respected +msgid "Not Respected" +msgstr "Non Respecté" + +#. module: purchase_requisition_proposal +#: model:ir.ui.menu,name:purchase_requisition_proposal.menu_others_purchase_requisition +msgid "Others Requisitions" +msgstr "Appels d'offre entrants" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_proposal__partner_id +msgid "Partner" +msgstr "" + +#. module: purchase_requisition_proposal +#: model_terms:ir.ui.view,arch_db:purchase_requisition_proposal.purchase_requisition_view_form +msgid "Planned Date" +msgstr "Date planifiée" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_line__qty_planned +#: model_terms:ir.ui.view,arch_db:purchase_requisition_proposal.purchase_requisition_view_form +msgid "Planned Quantities" +msgstr "Quantités planifiées" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_proposal__qty_planned +msgid "Planned Quantity" +msgstr "Quantité planifiée" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_proposal__product_id +msgid "Product" +msgstr "Article" + +#. module: purchase_requisition_proposal +#: model_terms:ir.ui.view,arch_db:purchase_requisition_proposal.purchase_order_proposal_view_form +#: model_terms:ir.ui.view,arch_db:purchase_requisition_proposal.sale_order_proposal_view_form +msgid "Proposal" +msgstr "Offre" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_requisition_proposal_wizard__proposal_line_ids +msgid "Proposal Line" +msgstr "Ligne Proposition" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_line__proposal_line_count +msgid "Proposal Line Count" +msgstr "Nb Offres" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_line__date_planned +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_proposal__date_planned +msgid "Proposed Date" +msgstr "Date Proposée" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_proposal__qty_proposed +msgid "Proposed Quantity" +msgstr "Quantité Proposée" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_proposal__purchase_id +msgid "Purchase" +msgstr "Commande d'achat" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_proposal__requisition_id +msgid "Purchase Agreement" +msgstr "Appel Offres" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_proposal__requisition_line_id +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_requisition_proposal_wizard__requisition_line_id +msgid "Purchase Agreement Line" +msgstr "Lignes d'Appel Offres" + +#. module: purchase_requisition_proposal +#: model:ir.ui.menu,name:purchase_requisition_proposal.menu_purchase_requisition_type +msgid "Purchase Agreement Types" +msgstr "Type d'Appel Offres" + +#. module: purchase_requisition_proposal +#: model:ir.model,name:purchase_requisition_proposal.model_purchase_order +msgid "Purchase Order" +msgstr "Commande fournisseur" + +#. module: purchase_requisition_proposal +#: model:ir.model,name:purchase_requisition_proposal.model_purchase_order_line +msgid "Purchase Order Line" +msgstr "Ligne de commande d'achat" + +#. module: purchase_requisition_proposal +#: model:ir.model,name:purchase_requisition_proposal.model_purchase_requisition +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_sale_order__purchase_requisition_id +msgid "Purchase Requisition" +msgstr "Appel d'offres" + +#. module: purchase_requisition_proposal +#: model:ir.model,name:purchase_requisition_proposal.model_purchase_requisition_information_mixin +msgid "Purchase Requisition Information Mixin" +msgstr "" + +#. module: purchase_requisition_proposal +#: model:ir.model,name:purchase_requisition_proposal.model_purchase_requisition_line +msgid "Purchase Requisition Line" +msgstr "Ligne de l'appel d'offre" + +#. module: purchase_requisition_proposal +#: model:ir.model,name:purchase_requisition_proposal.model_purchase_requisition_proposal +msgid "Purchase Requisition Proposal" +msgstr "Propositions d'appels d'offres" + +#. module: purchase_requisition_proposal +#: model:ir.model,name:purchase_requisition_proposal.model_purchase_requisition_type +msgid "Purchase Requisition Type" +msgstr "Type de demandes d'achat" + +#. module: purchase_requisition_proposal +#: model:ir.actions.act_window,name:purchase_requisition_proposal.purchase_requisition_form_action +#: model:ir.actions.act_window,name:purchase_requisition_proposal.purchase_requisition_no_edit_form_action +#: model:ir.ui.menu,name:purchase_requisition_proposal.parent_menu_purchase_requisition_proposal +msgid "Purchase requisitions" +msgstr "Appels d'Offres" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_information_mixin__qty_check +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_line__qty_check +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_proposal__qty_check +msgid "Qty Check" +msgstr "Vérif. Quantité" + +#. module: purchase_requisition_proposal +#: model_terms:ir.ui.view,arch_db:purchase_requisition_proposal.received_requisition_proposal_line_view_tree +#: model_terms:ir.ui.view,arch_db:purchase_requisition_proposal.requisition_proposal_buttons_wizard +msgid "Remove" +msgstr "Supprimer" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_type__proposal_template_id +msgid "Report Template" +msgstr "Template mail Appel Offres" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_proposal__schedule_date +msgid "Requested Date" +msgstr "Date Requise" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_proposal__product_qty +msgid "Requested Quantity" +msgstr "Quantité Requise" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_order_line__requirement_proposal_id +msgid "Requirement Proposal" +msgstr "Proposition appel d'offre" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_res_company__requisition_intercompany_partner_ids +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_res_config_settings__requisition_intercompany_partner_ids +msgid "Requisition Intercompany Partners" +msgstr "Contacts Appel d'Offre Multi-Sociétés" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition__requisition_proposal_count +msgid "Requisition Proposal Count" +msgstr "Nombre Propositions" + +#. module: purchase_requisition_proposal +#: model:ir.model,name:purchase_requisition_proposal.model_requisition_proposal_wizard +msgid "Requisition Proposal Wizard" +msgstr "" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition__requisition_proposal_ids +msgid "Requisition proposal" +msgstr "" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields.selection,name:purchase_requisition_proposal.selection__purchase_requisition_information_mixin__date_check__respected +#: model:ir.model.fields.selection,name:purchase_requisition_proposal.selection__purchase_requisition_line__date_check__respected +#: model:ir.model.fields.selection,name:purchase_requisition_proposal.selection__purchase_requisition_proposal__date_check__respected +msgid "Respected" +msgstr "Respecté" + +#. module: purchase_requisition_proposal +#: model:ir.model,name:purchase_requisition_proposal.model_sale_order +msgid "Sales Order" +msgstr "Commande" + +#. module: purchase_requisition_proposal +#: model:ir.model,name:purchase_requisition_proposal.model_sale_order_line +msgid "Sales Order Line" +msgstr "Ligne de bon de commande" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,help:purchase_requisition_proposal.field_purchase_requisition__exclusive +#: model:ir.model.fields,help:purchase_requisition_proposal.field_purchase_requisition_type__exclusive +msgid "" +"Select only one RFQ (exclusive): when a purchase order is confirmed, cancel the remaining purchase order.\n" +"\n" +" Select multiple RFQ (non-exclusive): allows multiple purchase orders. On confirmation of a purchase order it does not cancel the remaining orders" +msgstr "" +"Sélectionner une seule demande de prix (exclusive) : lorsqu'un bon de commande est confirmé, annuler les autres bons de commande.\n" +"\n" +" Sélectionner plusieurs demandes de prix (non-exclusive) : autoriser les demandes de prix multiples. Lorsqu'un bon de commande est confirmé, les autres bons ne sont pas annulés" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields.selection,name:purchase_requisition_proposal.selection__purchase_requisition_type__multi_company_selection__selected +msgid "Selected Companies" +msgstr "Sociétés Sélectionnées" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,help:purchase_requisition_proposal.field_purchase_requisition__schedule_date +#: model:ir.model.fields,help:purchase_requisition_proposal.field_purchase_requisition_proposal__schedule_date +msgid "The expected and scheduled delivery date where all the products are received" +msgstr "La date de livraison prévue et planifiée où tous les produits sont reçus" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_proposal__price_unit +msgid "Unit Price" +msgstr "" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_proposal__product_uom_id +msgid "UoM" +msgstr "" + +#. module: purchase_requisition_proposal +#: model_terms:ir.ui.view,arch_db:purchase_requisition_proposal.purchase_requisition_view_form +msgid "Validate and Call Companies" +msgstr "Valider et envoyer mail" + +#. module: purchase_requisition_proposal +#: model:ir.model.fields,field_description:purchase_requisition_proposal.field_purchase_requisition_line__proposal_line_ids +msgid "proposal Lines" +msgstr "Lignes d'offres" + +#. module: purchase_requisition_proposal +#: model_terms:ir.ui.view,arch_db:purchase_requisition_proposal.purchase_requisition_view_form +msgid "proposals" +msgstr "Offres" diff --git a/purchase_requisition_proposal/models/__init__.py b/purchase_requisition_proposal/models/__init__.py new file mode 100644 index 000000000..1e587f4dd --- /dev/null +++ b/purchase_requisition_proposal/models/__init__.py @@ -0,0 +1,9 @@ +from . import purchase_requisition_information_mixin +from . import purchase_order +from . import sale_order +from . import purchase_requisition +from . import purchase_requisition_line +from . import purchase_requisition_proposal +from . import purchase_requisition_type +from . import res_company +from . import ir_config diff --git a/purchase_requisition_proposal/models/ir_config.py b/purchase_requisition_proposal/models/ir_config.py new file mode 100644 index 000000000..e99f27135 --- /dev/null +++ b/purchase_requisition_proposal/models/ir_config.py @@ -0,0 +1,16 @@ +# Copyright (C) 2023 Akretion (). +# @author Kévin Roche +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class InterCompanyRulesConfig(models.TransientModel): + _inherit = "res.config.settings" + + requisition_intercompany_partner_ids = fields.Many2many( + comodel_name="res.users", + string="Requisition Intercompany Partners", + related="company_id.requisition_intercompany_partner_ids", + readonly=False, + ) diff --git a/purchase_requisition_proposal/models/purchase_order.py b/purchase_requisition_proposal/models/purchase_order.py new file mode 100644 index 000000000..69531948b --- /dev/null +++ b/purchase_requisition_proposal/models/purchase_order.py @@ -0,0 +1,25 @@ +# Copyright 2023 Akretion (https://www.akretion.com). +# @author Kévin Roche +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class PurchaseOrder(models.Model): + _inherit = "purchase.order" + + def _prepare_sale_order_line_data(self, purchase_line, dest_company, sale_order): + res = super()._prepare_sale_order_line_data(purchase_line, dest_company, sale_order) + res["price_unit"] = purchase_line.price_unit + return res + + +class PurchaseOrderLine(models.Model): + _inherit = "purchase.order.line" + + requirement_proposal_id = fields.Many2one( + comodel_name="purchase.requisition.proposal", + string="Requirement Proposal", + ) + + diff --git a/purchase_requisition_proposal/models/purchase_requisition.py b/purchase_requisition_proposal/models/purchase_requisition.py new file mode 100644 index 000000000..1ea11ae2d --- /dev/null +++ b/purchase_requisition_proposal/models/purchase_requisition.py @@ -0,0 +1,171 @@ +# Copyright 2023 Akretion (https://www.akretion.com). +# @author Kévin Roche +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + +class PurchaseRequisition(models.Model): + _inherit = "purchase.requisition" + + exclusive = fields.Selection(related="type_id.exclusive", readonly=True) + schedule_date = fields.Date(required=True) + date_end = fields.Date(required=True) + requisition_proposal_ids = fields.One2many( + comodel_name="purchase.requisition.proposal", + inverse_name="requisition_id", + string="Requisition proposal", + ) + requisition_proposal_count = fields.Integer( + compute="_compute_requisition_proposal_count" + ) + company_to_call_ids = fields.Many2many( + comodel_name="res.company", + compute="_compute_company_to_call_ids", + string="Companies To Call", + readonly=False, + store=True, + ) + + @api.depends("exclusive") + def _compute_company_to_call_ids(self): + for rec in self: + if ( + rec.exclusive == "proposals" + and rec.type_id.multi_company_selection != "no" + ): + if rec.type_id.multi_company_selection == "selected": + rec.company_to_call_ids = rec.type_id.company_to_call_ids + else: + rec.company_to_call_ids = rec.env["res.company"].search( + [("id", "!=", rec.company_id.id)] + ) + else: + rec.company_to_call_ids = False + + def action_create_call_companies(self): + self.action_open() + return self.action_call_for_proposal_send() + + def _get_destinaries_email(self): + missing_emails = [comp.name for comp in self.company_to_call_ids if not self.company_to_call_ids.requisition_intercompany_partner_ids] + if missing_emails: + raise UserError( + _("%s don't have contacts to send emails.") % ( ", ".join(missing_emails) + ), + ) + return self.company_to_call_ids.requisition_intercompany_partner_ids.partner_id + + def action_call_for_proposal_send(self): + for rec in self: + template_id = rec.type_id.proposal_template_id.id + lang = self.env.context.get("lang") + template = self.env["mail.template"].browse(template_id) + if template.lang: + lang = template._render_lang(self.ids)[self.id] + ctx = { + "default_model": "purchase.requisition", + "default_res_id": self.id, + "default_use_template": bool(template_id), + "default_template_id": template_id, + "default_partner_ids": self._get_destinaries_email().ids, + "default_composition_mode": "comment", + "custom_layout": "mail.mail_notification_light", + "force_email": True, + "model_description": self.with_context(lang=lang).name, + } + + return { + "type": "ir.actions.act_window", + "view_mode": "form", + "res_model": "mail.compose.message", + "views": [(False, "form")], + "view_id": False, + "target": "new", + "context": ctx, + } + + @api.onchange("schedule_date") + def _onchange_schedule_date(self): + for line in self.line_ids: + line.schedule_date = self.schedule_date + + @api.depends("requisition_proposal_ids") + def _compute_requisition_proposal_count(self): + for rec in self: + rec.requisition_proposal_count = len(rec.requisition_proposal_ids) + + def show_requisition_proposal(self): + views = [ + ( + self.env.ref( + "purchase_requisition_proposal.purchase_requisition_proposal_view_tree" + ).id, + "tree", + ), + ( + self.env.ref( + "purchase_requisition_proposal.purchase_requisition_proposal_view_form" + ).id, + "form", + ), + ] + return { + "name": "proposals", + "type": "ir.actions.act_window", + "res_id": self.id, + "view_mode": "tree,form", + "res_model": "purchase.requisition.proposal", + "target": "current", + "domain": [("id", "in", self.requisition_proposal_ids.ids)], + "views": views, + "view_id": False, + } + + def action_create_quotations(self): + lines = self.requisition_proposal_ids.filtered( + lambda x, s=self: x.qty_planned > 0 + and x not in s.purchase_ids.order_line.mapped("requirement_proposal_id") + ) + partners = lines.mapped("partner_id") + if lines: + for partner in partners: + fiscal_position = ( + self.env["account.fiscal.position"] + .with_company(self.company_id) + .get_fiscal_position(partner.id) + ) + proposals_lines = lines.filtered(lambda x, p=partner: x.partner_id == p) + po_lines = [] + for line in proposals_lines: + taxes = fiscal_position.map_tax( + line.product_id.supplier_taxes_id.filtered( + lambda tax, c=self.company_id: tax.company_id == c + ) + ) + proposal_line = { + "product_id": line.product_id.id, + "price_unit": line.price_unit, + "product_qty": line.qty_planned, + "date_planned": line.date_planned, + "requirement_proposal_id": line.id, + "taxes_id": taxes.ids, + } + po_lines.append((0, 0, proposal_line)) + + self.env["purchase.order"].create( + { + "requisition_id": self.id, + "partner_id": partner.id, + "fiscal_position_id": fiscal_position.id, + "order_line": po_lines, + "company_id": self.company_id.id, + } + ) + + def action_in_progress(self): + super().action_in_progress() + if self.type_id.exclusive == "proposals": + self.name = self.env["ir.sequence"].next_by_code( + "purchase.requisition.purchase.call" + ) diff --git a/purchase_requisition_proposal/models/purchase_requisition_information_mixin.py b/purchase_requisition_proposal/models/purchase_requisition_information_mixin.py new file mode 100644 index 000000000..c2f68d20a --- /dev/null +++ b/purchase_requisition_proposal/models/purchase_requisition_information_mixin.py @@ -0,0 +1,51 @@ +# Copyright (C) 2022 Akretion (). +# @author Kévin Roche +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class PurchaseRequisitionInformationMixin(models.AbstractModel): + _name = "purchase.requisition.information.mixin" + _description = "Purchase Requisition Information Mixin" + + date_check = fields.Selection( + selection=[ + ("respected", "Respected"), + ("not_respected", "Not Respected"), + ("none", ""), + ], + string="Date Check", + compute="_compute_date_check", + ) + + qty_check = fields.Selection( + selection=[ + ("enough", "Enough"), + ("not_enough", "Not Enough"), + ("none", ""), + ], + string="Qty Check", + compute="_compute_qty_check", + ) + + @api.depends("schedule_date", "date_planned") + def _compute_date_check(self): + for rec in self: + if rec.schedule_date and rec.date_planned: + if rec.schedule_date >= rec.date_planned: + rec.date_check = "respected" + else: + rec.date_check = "not_respected" + else: + rec.date_check = "none" + + @api.depends("product_qty", "qty_planned") + def _compute_qty_check(self): + for rec in self: + if rec.product_qty == 0: + rec.qty_check = "none" + elif rec.qty_planned >= rec.product_qty: + rec.qty_check = "enough" + else: + rec.qty_check = "not_enough" diff --git a/purchase_requisition_proposal/models/purchase_requisition_line.py b/purchase_requisition_proposal/models/purchase_requisition_line.py new file mode 100644 index 000000000..8b72717c1 --- /dev/null +++ b/purchase_requisition_proposal/models/purchase_requisition_line.py @@ -0,0 +1,109 @@ +# Copyright 2023 Akretion (https://www.akretion.com). +# @author Kévin Roche +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import _, api, fields, models + + +class PurchaseRequisitionLine(models.Model): + _inherit = ["purchase.requisition.line", "purchase.requisition.information.mixin"] + _name = "purchase.requisition.line" + + proposal_line_ids = fields.One2many( + "purchase.requisition.proposal", "requisition_line_id", string="proposal Lines" + ) + proposal_line_count = fields.Integer(compute="_compute_proposal_line_count") + qty_planned = fields.Float( + compute="_compute_planned_qty", string="Planned Quantities" + ) + date_planned = fields.Date(string="Proposed Date", compute="_compute_date_planned") + is_proposal_line_validate = fields.Boolean( + string="Is Line Validate", + default=False, + ) + + @api.depends("proposal_line_ids.date_planned") + def _compute_date_planned(self): + for rec in self: + date_planned_list = [ + line.date_planned + for line in rec.proposal_line_ids + if line.qty_planned and line.date_planned + ] + if date_planned_list: + rec.date_planned = max(date_planned_list) + else: + rec.date_planned = False + + @api.depends("proposal_line_ids") + def _compute_proposal_line_count(self): + for rec in self: + rec.proposal_line_count = len( + [line for line in rec.proposal_line_ids if line.qty_proposed] + ) + + def create_proposal(self): + self.sudo().write({"is_proposal_line_validate": True}) + self.env["purchase.requisition.proposal"].create( + { + "requisition_id": self.requisition_id.id, + "requisition_line_id": self.id, + "product_id": self.product_id.id, + "qty_proposed": self.product_qty, + "date_planned": self.schedule_date, + "partner_id": self.env.company.partner_id.id, + } + ) + + def create_and_modify_proposal(self): + self.create_proposal() + return self.show_requisition_proposal_line() + + def show_requisition_proposal_line(self): + if self.env.company == self.requisition_id.company_id: + domain = [("requisition_line_id", "=", self.id)] + context = {"received_proposal": True} + view = self.env.ref( + "purchase_requisition_proposal.requisition_proposal_wizard" + ) + else: + domain = [ + ("requisition_line_id", "=", self.id), + ("partner_id", "=", self.env.user.company_id.partner_id.id), + ] + view = self.env.ref( + "purchase_requisition_proposal.requisition_proposal_buttons_wizard" + ) + context = {"my_proposal": True, "active_id": self.id,} + # return { + # "name": _("proposals"), + # "type": "ir.actions.act_window", + # "view_mode": "form", + # "res_model": "purchase.requisition.proposal", + # "target": "new", + # "domain": domain, + # "context": context, + # "view_id": view.id, + # } + + wizard = self.env["requisition.proposal.wizard"].create( + { + "requisition_line_id": self.id, + "proposal_line_ids": self.proposal_line_ids.filtered_domain(domain).ids, + } + ) + return { + "type": "ir.actions.act_window", + "res_model": "requisition.proposal.wizard", + "res_id": wizard.id, + "view_mode": "form", + "view_id": view.id, + "target": "new", + "context": context, + } + + + @api.depends("proposal_line_ids.qty_planned") + def _compute_planned_qty(self): + for rec in self: + rec.qty_planned = sum(rec.proposal_line_ids.mapped("qty_planned")) diff --git a/purchase_requisition_proposal/models/purchase_requisition_proposal.py b/purchase_requisition_proposal/models/purchase_requisition_proposal.py new file mode 100644 index 000000000..574b5e0fa --- /dev/null +++ b/purchase_requisition_proposal/models/purchase_requisition_proposal.py @@ -0,0 +1,101 @@ +# Copyright 2023 Akretion (https://www.akretion.com). +# @author Kévin Roche +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import api, fields, models + + +class PurchaseRequisitionProposal(models.Model): + _inherit = ["purchase.requisition.information.mixin"] + _name = "purchase.requisition.proposal" + _description = "Purchase Requisition Proposal" + _order = "requisition_line_id" + + name = fields.Char( + string="Name", + ) + + requisition_id = fields.Many2one( + "purchase.requisition", string="Purchase Agreement" + ) + purchase_id = fields.Many2one( + comodel_name="purchase", + string="Purchase", + ) + requisition_line_id = fields.Many2one( + "purchase.requisition.line", required=True, string="Purchase Agreement Line" + ) + partner_id = fields.Many2one( + comodel_name="res.partner", + string="Partner", + # related="requisition_id.partner_id" + ) + company_id = fields.Many2one( + string="Company", + required=True, + default=lambda self: self.env.company.id, + readonly=True, + ) + requisition_company_id = fields.Many2one( + string="Company", related="requisition_id.company_id", readonly=True + ) + product_uom_category_id = fields.Many2one(related="product_id.uom_id.category_id") + product_id = fields.Many2one( + "product.product", string="Product", related="requisition_line_id.product_id" + ) + product_uom_id = fields.Many2one( + "uom.uom", + string="UoM", + domain="[('category_id', '=', product_uom_category_id)]", + related="requisition_line_id.product_uom_id", + readonly=True, + ) + price_unit = fields.Float( + string="Unit Price", + digits="Product Price", + related="requisition_line_id.price_unit", + readonly=True, + ) + product_qty = fields.Float( + string="Requested Quantity", + digits="Product Unit of Measure", + related="requisition_line_id.product_qty", + readonly=True, + ) + schedule_date = fields.Date( + string="Requested Date", required=True, related="requisition_id.schedule_date" + ) + qty_proposed = fields.Float( + string="Proposed Quantity", + digits="Product Unit of Measure", + default=lambda self: self.product_qty, + ) + qty_planned = fields.Float( + string="Planned Quantity", + digits="Product Unit of Measure", + default=lambda self: self.product_qty, + ) + date_planned = fields.Date(string="Proposed Date") + + @api.model + def create(self, vals): + vals["name"] = self.env["ir.sequence"].next_by_code( + "purchase.requisition.proposal" + ) + return super().create(vals) + + def duplicate_line(self): + self.create( + { + "requisition_id": self.requisition_id.id, + "requisition_line_id": self.requisition_line_id.id, + "product_id": self.product_id.id, + "partner_id": self.partner_id.id, + } + ) + return self.requisition_line_id.show_requisition_proposal_line() + + def remove_line(self): + req_line = self.requisition_line_id + self.unlink() + return req_line.show_requisition_proposal_line() diff --git a/purchase_requisition_proposal/models/purchase_requisition_type.py b/purchase_requisition_proposal/models/purchase_requisition_type.py new file mode 100644 index 000000000..320655a66 --- /dev/null +++ b/purchase_requisition_proposal/models/purchase_requisition_type.py @@ -0,0 +1,51 @@ +# Copyright (C) 2022 Akretion (). +# @author Kévin Roche +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class PurchaseRequisitionType(models.Model): + _inherit = "purchase.requisition.type" + + exclusive = fields.Selection( + selection_add=[ + ("proposals", "Call for Proposals (non-exclusive)"), + ], + ondelete={"proposals": "cascade"}, + ) + + company_id = fields.Many2one( + string="Company", + required=True, + default=lambda self: self.env.company.id, + readonly=True, + ) + + multi_company_selection = fields.Selection( + selection=[ + ("no", "No"), + ("selected", "Selected Companies"), + ("all", "All Companies"), + ], + string="Multi Company Selection", + default="no", + ) + + company_to_call_ids = fields.Many2many( + comodel_name="res.company", + string="Companies To Call", + ) + + def _default_proposal_template_id(self): + return self.env.ref( + "purchase_requisition_proposal.mail_template_requisition_proposal", + raise_if_not_found=False, + ) + + proposal_template_id = fields.Many2one( + "mail.template", + default=lambda self: self._default_proposal_template_id(), + string="Report Template", + required=True, + ) diff --git a/purchase_requisition_proposal/models/res_company.py b/purchase_requisition_proposal/models/res_company.py new file mode 100644 index 000000000..e75ebb094 --- /dev/null +++ b/purchase_requisition_proposal/models/res_company.py @@ -0,0 +1,17 @@ +# Copyright (C) 2023 Akretion (). +# @author Kévin Roche +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ResCompany(models.Model): + _inherit = "res.company" + + requisition_intercompany_partner_ids = fields.Many2many( + comodel_name="res.users", + column1="company", + column2="partners_to_send_mails", + relation="company_rel_partners_to_send_mails", + string="Requisition Intercompany Partners", + ) diff --git a/purchase_requisition_proposal/models/sale_order.py b/purchase_requisition_proposal/models/sale_order.py new file mode 100644 index 000000000..39d3786f6 --- /dev/null +++ b/purchase_requisition_proposal/models/sale_order.py @@ -0,0 +1,32 @@ +# Copyright (C) 2023 Akretion (). +# @author Kévin Roche +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + purchase_requisition_id = fields.Many2one( + comodel_name="purchase.requisition", + string="Purchase Requisition", + related="auto_purchase_order_id.requisition_id", + ) + + def action_confirm(self): + for order in self.filtered("auto_purchase_order_id"): + if order.purchase_requisition_id.exclusive == "proposals": + for line in order.order_line.sudo(): + line.price_unit = line.auto_purchase_line_id.price_unit + return super().action_confirm() + + +class SaleOrderLine(models.Model): + _inherit = "sale.order.line" + + requirement_proposal_id = fields.Many2one( + comodel_name="purchase.requisition.proposal", + string="From Proposal", + related="auto_purchase_line_id.requirement_proposal_id", + ) diff --git a/purchase_requisition_proposal/readme/CONTRIBUTORS.rst b/purchase_requisition_proposal/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..dcae277c8 --- /dev/null +++ b/purchase_requisition_proposal/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Kévin Roche diff --git a/purchase_requisition_proposal/readme/DESCRIPTION.rst b/purchase_requisition_proposal/readme/DESCRIPTION.rst new file mode 100644 index 000000000..eb42f57b0 --- /dev/null +++ b/purchase_requisition_proposal/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module allows to diff --git a/purchase_requisition_proposal/security/ir.model.access.csv b/purchase_requisition_proposal/security/ir.model.access.csv new file mode 100644 index 000000000..29984209b --- /dev/null +++ b/purchase_requisition_proposal/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_purchase_requisition_proposal,purchase.requisition.proposal.user,model_purchase_requisition_proposal,purchase.group_purchase_manager,1,1,1,1 +access_requisition_proposal_wizard,requisition.proposal.wizard,model_requisition_proposal_wizard,purchase.group_purchase_manager,1,1,1,1 diff --git a/purchase_requisition_proposal/security/purchase_requisition_security.xml b/purchase_requisition_proposal/security/purchase_requisition_security.xml new file mode 100644 index 000000000..7bb56813e --- /dev/null +++ b/purchase_requisition_proposal/security/purchase_requisition_security.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + Purchase Requisition multi-company + + + ['|','&', ('company_to_call_ids','in',company_ids), ('state', 'in', ['open', 'done']), ('company_id', 'in', company_ids)] + + + + Purchase requisition Line multi-company + + + ['|','&', ('requisition_id.company_to_call_ids','in',company_ids), ('requisition_id.state', 'in', ['open', 'done']), ('company_id', 'in', company_ids)] + + + diff --git a/purchase_requisition_proposal/static/description/icon.png b/purchase_requisition_proposal/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..add7c1d68d19b39265c1647cf5ced3ac7183a5a7 GIT binary patch literal 25842 zcmYJaby$<{|2|ANOwrLH0t$>#3Mw4}(mlqg(W66{do!9F;uk#g)*49*}reLDL!^5LidG%5k4-cRJ_C-d5d&l=} zoEIM613ZdH1I(MLL4G2?ZEuXOJE zyZ)lJ5)@|2>1g9|crW!mlvVD&Pdurl+0|;_&epeY-)g;gmy&Cuquow+e*gY@)_?Q$ z#gjzB*a60qokmp~ul9kB!p#$su3|IOLD!cwS(?YgCUF6+Z^b1>0C!4r4fz5eG|&ms zyOh>y)>rJ*{!RJ8FE;ke@#~xcPlLvU#!7UfH}XWYWKV1WJ1uQwC*S^eCw}h5m=%;E z-9t~}6_omyL$k}0EwIY|x~^@Pe^bZqNnshb$$N`^xjl(h$mn=pcl*%ltwe#3o_|^9 zp5o&sqk3e;x?@-KTEBCD{7EU}d0%sS##Ifu@=5&PM<)RI@WFa^E{dD*GBNbW14KJhVX@T&vT$-T^U9;)iL_2d77LVK zuG_}=W_7jjV3IRACprr9U9(|`)-(Eft$*%{^^>T!vt4Pb6UKcmD0;KiU-dzYlZ-JH~&xjeShyjh^CG<}&6?aoN+ zqf^UoNo5#7-wyEF{-Fm8*c!|W8t+KoGwWk>wt^wQIc4e?53;MO^<)Q)Xxle!{6%~# zAA09CHtRc%udy}r{mb0?_isJNvw<^Oy&k!or>p5x&nZ{I*GzN*pVcgYS05c2!n_e9 z-8Luv@%jf4NC%qe;t6BP@x)?IcZ2f|Jug^8D`mDbcPp;hmcC$*B=*L|&u(p(b@txw zHVbJ#-rq^@p_f7Tn#x%i^b~rDnVC|#>Q6IwzSz2siN1NgDkb4se-N|T&@XTr(0sAi4%l5Mu`Wf| z4<$@S8N$vS_EkY%J%mURH&dt=MqI}x#US-V%Y*_2viIgZU64=Bv4HMkZraeoWywy7 zKDY|fXi-0OpxUd@s}MUHtk@;rPIKw+2^eFlebHcGth^aSGjq@a_E_4PaLyHlm^g|7 zK#-Y7t%q0&ruMvydjG#Otpy|j>L6RHO6_R%8lluH`#! zyKHv&nqIond(D5Yk>yVchvUp>wT;q_6-?!7VMbw>&F=&|D`jFAQOw~z{Wjg}H-|k{ zS&SNJzu^`ybmh`56nxELAt%$WJ1ip;Sbpnr|J>XS*TK1|>bKG>&60jSC41NdY=T^o z`O2NH?Vw?-W7>N-tDH~ z)(|N=Pp>{4qwU^doAQ)|f2QBlueV3a(s|X}7prQ>{x04y-u{j#V~v);wrO`RIoub) zDE^+{83cRW>+_?+u22F1-eptT;n<}uoPKtGB7Mh|YU8swTaj}5=554)3h|qcg$gNt zw$O%|;(i=+>Ygx|eOT2|;&y*>iBtZA-(ZIJpzBZ0z`HTUsgV{_QiR@?<_hrWpN(YA zTUV~~@VJYz^pez2%_Z&q%un!LrT|Un`FSc)~R$2_V_eXs%_B}zZ zRY%>2ihuuE8yA(LR087bUtUB(zxR9LkO)qYk>|IF_+)xCuYt#jgRYx*X+|L;=5Apx zaoMHT+nz8jU-)sBTFtu>3ktO}PWE>v!r-RYKgBBT?;X6#aS4q&Popz$5{_8OR&;d_Fui&B;J%Uwwx_0WY`@4s%a=YD^9+-+vRQdi4cV)2C|TCUZu zO^wl^&?-^t+(y!*%=4L+Zx%)#B;@RE5TBZH zoI-$#1t74yD5o@>Ty0ea9&6tfq%G{R#qRa`Y;A)c-2Qc0wYVP3Kxk z&u5#TLl3$&q9Na<&^udjIoaxC>WeVK>uBcQOvKXSwkU<;dii$89hAI4N8jgD28Kld zZo;V_o2EhF4@FD3@Wusr1%9}T@J#Ep^w^2k&gdPwcAQwL(G6g0-$>utCEQmGrz58O zFc)p?>4Rujx?_Lkg989ZFw;WxL)_NcX`3;@4=&r#a~0+*s=ve1^8101;)9Wf+0 zno`x@Ya)l!I}>!7$me7yPzI>?cA^8+)uan>-)&VjB3Zmgp|*MLgHRlG$dP{UwT@yK zvz3kZ;%<7c%xFBgj?l}<=5=lc6)*CE%T7NN&6tLDk_>eJ`MWq^S~^x!QuclSDeJ$Q;GQIcrOH+JRkUv3`M z(BRSJRC@zPe!Xl`4%nULl&0YafC0rq>;rXCvr@|q0=In1wP}Ehliz-TyBesU7g(?l z_dL%7wxeDuPmkp8hxotV}{)QC)zjYPHN zFyH50QvzwDguS;n|F@Iv4d5oQ^=(Ic_?ypZ)fg-M(Y@t14%|r+KR0#CM4{sOVT~-s zklaK^HrVlXbsrEH=Ip}j}{=m=N&Pb}-A8iu5(c=!+R^#zMHf1Z_Vp&64bPHVv(h+^+=75_8I{_GEQtpxf9 z=HPZ+BTG&!8?#bRivW5-2i>{H-MOmp&wNJQ`<_jQ7xujGjX6>)yw-)Dw6}Ktpo7*; znyh*IzO$Oa1=ZUq1K@~`>WMZb4zT}tC#zEM08qRr-%IxYAOwJussG}czL%{B;`ZWy zS1mER4NdaRf@z6KdWEA<+RD@&xSgoR3ZK_n%1*Y)HM{@qg$4_+Pr(-&(_AzXNCztX z#p#FeMSNn~E02nS#fw-|_cCgqyYCBoX))rpcyQ0ksYgK#Mi|g=W+U0n zheLvddo?*FeMlazgXf26=le}O&j#}&9mFvPC)hMt`y@I8=$0)T%*CDj+Tk`{lBK#F zI_%m=D;6n=8Bp~@x^+*_chZmn|Lx0kyO1ynd8~Qy(>`dRS4=KgRola9>@%COC>b*?R{}+l4{?+v}3XlKaGkpUKyLz>%8FrvO$dKWJJ2;7o&>@J@nZ8}Z7R8AW0q$9TRKg#eekMnH1B%7s{-cr+ zbzAU%a#n;B_36jZ%Potcgfg^qz!Rw3g>Xfag zh2A>!+iQjf=9Hp6B4tH60^) z{;~1W?)hm=R65lZHO=^97`z6zU1TA2}3a}1=c%QCuJ@<`KCbZx|t2X4bh4tkHT>B2uB6uQ<1tu60>CK2mVfEZR%p9y&55KKx1G z8+NLlrtf9nZ}o|Q^9@ciaMbVuqbH}tA42yrJalNLFLaoRF?Jer(S~$wf|0P-+RxB0 zVfd{evG6Cy^|5{Wqs%j;x5vp-tSU7>ptvLiV(duD(qLd_6n&H-ieCMFL#O%KldR7% zBiX09*5U<&kcb<#Vs3@f(HpnP0oRb2vcmCQPmwtsX?uY%Vw~I;Rbw5T*MqGp<62{+ zele(S9!_MooOU;bK1xgX`E?Ci%L+`cF0-^5N8PZwwd@$m8`;M6r_))Xg+tDVpiD*4HOdso$OV3 zn}xnEWuxTw>a|?y2w0GFOp)#Xh$<(ynRBj4oDXKz9PMN42%5)n+kgkngQDf@R_JB> z?URcyFMx%OPUCmFdUIaRom>6bNk+nPS~$>fz+>Vu%&Yz4L1ujT0LI{nf>NryK?AmX z&$L_SqV)CMOQ*xuKX96+yH-xOD~^x_;lc(|&xA8a#PUP6$&y*V_M~4?S08jXLi>jj z<)>StGV1P?TrQnyKjGQgvEXs&DxfX+n@UxZD$9H6x5WG7J-+r?y!c8p2%PRGV!Bk7 z$8lUeb|m^s4*Q63f(ujc&r{05x|~7_{npj2O7zKI3_BuKzr6rj_^q5Tzder!K|V<` zAfPhMC+aCYR1Hq9qvBhrc6N!>7(M+Y0_al!AztXoh`cSWRnCl}BZz$lP45xI2e|_h zP42pkkHy_FF8e{KImuCq)bHV@M0k06Vu0S?DC4iL*b10w=fL<+Xd@pW`4U~!3{+&E zuPA%B79>B5j>+eXzZK(X&PVVff_;4pVM@>JTi;GX;SNZBdKY??$pkUtD^3};*}GuY zjHEXY;?sp7w}2Hd2tZ}XT7L4P+UYybm-0_q3l+vRpVmdCe1Cje5v7#-nFJ;b$E6-A z2ai6bk-*gGyfEDo50M=Vsgrx05T@J;ANzY>y;gSTW!M{On`68xHbS_C5Y7Ze30z2$ zBqWL5ZlDZRF?-2V*;`JOOs;U?|3`|^gbo^pszZ%m^A(iuOiE0#r-bh_J$VBDozKfL~NCCKOpzZpU8Oo?7@$X-F4vs z#C|8*(pO(2hUB5&>h-daFBQD-KlNA41>T6hPxm1U5VeC4K)gT+xni6LS&O|QWgg7W zuhy#My=;p0rF{@*#rD>}MlC^LZj@i_kho2hKfJ-9axijQKiP>{a@93(s@D#a{yOuW zOni=so&pG|QwKB&Y+D)>Z4VvoE0qq_dvs0F=$e)uY;a@Xfel*uQP@(391D8JP}M1_ zZ3fHsGPn>yKax5GrPb_d6d$md)zN&dn7aDMBvzf4l%&BIi=K_X^f~DbYGHMa6ZDB3 zx<89;APh8AN7#pq3}=r#%_kI?xVbwHU#{#>;(toh%6}3`uN5-Z=*x2^NdOKvIJ{(W zLQRSZosGpKKFladt-rsRVRXf;8?xYkh^5zs{&E!de~am&E#Rj7Y?E0W$wJh*C5Er+ z;hR?DqM?T{-4K82Z6qYM{#n7owv{>hhXv&LO&Qt&u|^^k?YL&Cu-TN^K=w>SW|m8o#Kr)Y!8qhtt}Y2g!3b+cKiGn<3CrAzHC@IXY(=S5D$ ziE-};)gX;%tlvu0Kw_?#a%)-2YFZf|FZX_7MVD^6+=0$sXAv7|)b(Fadt5?8EEVeY zhYaeeAD`2dQ*rOhDju>?o(0+tfw=}<(@vjGx~r}T&}dgW2%eih3r^9o_|x?RU>EinJq0G9ic(akV`c-`f7{E0Hs+vpX~ypUSL` zsE~B8hqvHf_@JPYIi6MJrE_1NsmVy=DqBZzyBuXQu!TZv18UXIg^D2u6OC#N$H&%K z|JJ507@e6`aD)>BzXdVtDZYD}diCNBK<}w>bqHTgQWWr-D5PSrYx^sW9M4PpXpB4* zY^*J>OG1Wh@&19KHIctQGLdZ#J3TXGkD@GNASUsy)gzxuOARkDOXCT-TUSOWbeqg#!>5nLspJW9 z)8nfZ?X(IWcF+8tjj7YDI10yuyBlwQGr=TLCKrW2HESu+Uqe=4cTI`GjL5x-pyz+@ zj(h%lqa$phwDJ^Nd*1E7ml+wwu}V^gToV$0{Ra!S=oyS>OjLB16K-1YKOJGL!-Oi% zt))(Vv`ZNyn%J}fcH{Xgme6TV?7I*QfDtoki#C-uI@Bb!wU?Kh@#|(LbN%^^_jASB znj>dWHZ$EDzaM8|+HVlwo-lT%pMZwOo5DngBxQNxO^GU$&zk zV1Czxe8M~=k#-=%9~F+bA&Du^Vn87l_4ZOM^JU@I4z`V=O ziFi$N6zGdPy01HLLdx^5>6p@)9zuWdh|Nvxr0vl&h+1B`5Q0hg!$%fh9H>x{@B`Zp zwaGVTtYaUIoSa7qwtU;o{8*4POH}*^vmtBs8t(cHP@Z`Ux zXBXbNLw+BUt}Y$pLRM}1Ev9rdxDCh(%HkxJSwIz*!)Hbf=?|LcVoRBcM(s3)IiG32 zVP}KpiP&VL??4Ms%Pzi+Fpb@<%eVBu+31LnE7ZkgkU{O_@DvItPGFZWv_gY^2k^}8 za;<9?ph_ggGM~f9(1CA%yX+l(2Rw8{Hs^{tS`hKnq>g+X8g$z+J82F6(o8P!MiiZK zwbm-MKRUclB)8Xm(BsBRrBvtKB>2^POFxHpo$q9CQRaqXHuI?V%|nN_Eb94#oPTF; zE}7{ma8LqS{3C{#UYmL~8*|m^hfi}Y1vx(PPYX^afQiF_gs@Z%PJxBaAltXv=e2#@ zp6PjSU*MqyGvUX`%c-9Ed~&d$WXPDAX*4>!nC_wedV3t9y4gbfQ-~@ zkck?>)^wWIwJ1ZLdllJqBBgU-@r!fE=hq((=P@(!>h-BxzNYxahdYs-Cs3ir}^24oM-08YyfZkZMK-1;_{IPLQX@ zTL1}))m)@#%mN;3J1~?p)s|d(u8wx7R>L#xh5Sx!^KAM3)Hn0&6XzTAUFMTi=1CIx zNxr-_pU9MrZM)tJi|Q0OW6R2W%QOAt%(tM3!~7skPer7j)9#~K<%h8;lg^SCYsU)n zOS=h}#S2=(6em2a6sUNJHe`f38;D<&FWXB7!iR}rdT3$yp_!2sY9XX-&@#WOXil7= zZ=TIB1*jQ+c?D;MM^JaKwkP2EFP#*-T?!bsYKhS#rLE6e`l^pj5zw19^}5P1UmqM~ z5$OAlofKC@8`RR zv#d`||9CYLu-piCt*_1WIv z>j}E)cm>47Tt@AYxp9aIFw@F-jS$Y)6Yho&f?Sy{3MFJ?Yr_jrPwy3RlDfV!mA_8R z+p5HVxo)P6Mw75*SzgutGnf*2Il!a)KpG#pLjf*Cc1_B5hWnhh;>TlbH(gv~pFcK~ zAKe%1WEjoIf1uCKh%(7Ontxq|>DMx&GO$*5s(|kUljVw=6dO{@qg) z{UzmT%V)z&x>@dEn;wcWy@yD?O}z8F&=nyW4aB(I6(pFFrj}WWiWywo@sxFLw9+|= zjt?2X7V&w9BIjj9#Mv7a?Y5wg$^P#sHjljz9m(6*jw2@&cu`KwkTG7QHzne+IIzNEU=cS z!qS2$e@*{q;-8FIoMVg8tKc-kd`)pG+qYLb)6`s^g&cKXM@zWcG+P7fhcKVq7C25Y zsPhL$@&>8B_h5gxxhOz<=|r$T?xe7X{s@c30vc#xt>=1C-u1^*X{+2^B8YbgT`5vM znLCW0z;W2CGQX#L<-~qQfrQ`mYCYJ7vUs>u`u=R@1ud1aV1{c2!VJXE-BXyvUpDKs z{cZjvis(Z4r_z+A+~($s%)y50#;fZhB4lB=(<2u)9>@5RsA8?85mRk*2`Hit_Lv)iM|H&t5QSo}U3#@51;`+&b2FTkR78VG*%`uP_vL1UZR6 zh>yZ!TLxxs-jwz7?gK?X0-IoLsF_)3tZ%76G^@koKEL7|Ov+-F8wQ z`U!Jqt6@ZEZz|I)%kfJe;lAr91%cnSi{9499x5yl>!O%I&!|Yh8~OY8$!gv0SCy47?E8S)FnO`3nD`${*a4ZmrW`UGjgk0GH5N{!v=ZKp+14+yTL9PtejB?A}@;3WOxNPptYS#r()`jGTMH;{ermCI4Ir)P|V}5 zZ}%`F5ZzJVgM|n|JLWI+Q?I6q{Y)vl__d47^UUXk!8m#3 z?Id!P^78r&M63Hk^Ul|%GJ?2MPs`UCqjy=8Q<$KpMBqGRl7H+=R--3L5$4@|y(Ca0 zc{QRGDx6xOso7#(AjXah6y{!l@Bt;x;{^ZOp3W z{uk%=+6SIArk8wsFOQSpvd@3I2w-Bq&_`LG{s_hLa6Y#C?#Fk$DcW9K1i4TTSMGBkcO{QSs^rho6qQuh8caBE>e|^= zlDASp3u)ql27=qo_0)4C9La@;Xa2;i&w1M+)`4_S5+BN{g#-PQeu|i8uMi;lvvLXO z0AS(2E>nYT!Ye^d9qHK19^3Y1ZSv;ZC35XE#K=m568Yms!7a z>UWF`XNHQt5qkt74JIt!-0}i+C$e|-t)W>DCA6VupD$CfvzQ#DCq+e{GkHlY7 z7|OU)JYC{@ldV@#MYLb^W*V(^3ZG%|$4=}O3Cr2NH!~3Sn;5m>P{qaBsr0GcUFfA~ zyEe{#?Iws*IAi`i)|l&^<^9WkCMGn~dgia7-pd9>|L6%!6OtWCegyqB_u1?Z1gAM- zlw-6TYV0>+V6?Wy)$6=K|IZa9t~7e_+|3Ruok__$cGD;I&l#^TcO4^I)^48P=q3mMeJT=a-@_+*9}=7Qn71FJ;goaQ~6!1n~8zX-aC0uyr;LhveWq4F6GH2sID zx|EW%8ls2>Dy(Dkp~1o*VIpo$0j8>d)*s#EtziJZw4Q2M=$v{V@TS8a3u zvGmRAJ)f20re>>wInuf7Jz)RWPFe9Hd49W@9wNxmfzL=!*w{nL(%p#cBhcfG2KRM> z>dOVIg$6-04Oz+?%YECJD@f?BLOgwD3F%C>>0)fzl`g@ z-ciG7oIoFBW}A(#{bzHL@#x6V0%ko>uN zH43n}p+@o}QORJ?i4Z>0j12r8 zRrr_Ve%xX!IZVuvFu6G9L+M0adS$fw^N6?PArTa@O;Mn1(d^nbk5Vi0vG=hWF>K( z$g8qQIt>bEI!$W z0VbEM7~S`d!upZh?@vr!i~Hw?+@H06#0PmYqW}JUaTu=mnrJFV82Rh0ZO~F7R-oTk zt3lDjHBIV0eyrDD{%Hj>BNb&g(@p!v;X;$#@VtYyxY$fhWS9q}7}6wp@a%4cvwJ+I;jRKg%#f zD7X4s%wMkB4TP;-`!|W)nZvaiY-z{yNd4zM((dRM5JoeE>n>Utd@QFa>b93W-TjjT zY@5gm1huSRm@22}d2{-rdf{`qh1~G;sTI*4x{4$wHcuew zAu_Zgc#B7X5gAy&AF?M=mTvEC;8y)pL)j_<6*3wm{T4q@@(rz&w=m!W56Lj*z}0*z zbo2Da#p3H8ecd&QrsTf>k5eV1!KBd%>;1#`HC&PHHF=iC2bc{C#KI5@ZmG=KxMYk8 zG0{h2O9MXZ^LxHn)yhs2Kp+Mg60)FU@(?M;k-p7Zd1!Xp(wwDCbqpHsQuG1%0WF?z*Qo=)MKdUpRO$X>x z7CyR6N`{gS6rw;4)3B?A%eJE!Qo0qpC}5{PUljw6a_r`FPuN3Fq;H;~(TV@OJ&C!6 zL3v1_If~>B?D+z!|L=^&m@`jL;O4@oIyoO-EN9S0urBFd$5@DFaDuW&40QVt=^088`Mw_|`*Lz}pUoNBr zp13~xZ2Fb-gBqP~a;PKCrXy+dd?2W~D|@r0z(9=SV-LHnLt<-+C|dm0$L$RIN6ky6 za|yw8DU7BHePbKTk7%Kce4xVluQ~ZxSom2Dz@_%Ot9w9z^#fxPM3v0EY3aX{-v@acNhF4pFi z3>=RVlzictU{apvx>REu(BM+O|J!Yld@Ml&|3annA^m>_o3iRv2kwV=z5zyjaCo~E zBuFA)fz(|L+FDobC5JjKzeL#JgX|FYaK#QNMbXgE(1ZAQbSZeT)wuf6c&8%kxP08R z=>v zDQBba%Y4>PWT)@DwW&JN5f#7Q;fl^FQ}>?CYcAHYF#QQ#X0xI=3;DTiG&elK#fO}- zj?QW5lJX>>gD{j_=buT`X1DH3ENx{IBZX`g^H+5`!pCAUWf9u2Ttz$bLGCs#9de`_ z;8;hYDtdc;Cp-BW-ISlJvnz+=oq-7Y*-x%3t;bPzvH52gqaU491tqVU#$6KG^CNNj zlvbO%_l^r0&ah2lcqnt*P2|@P{m5}`fr@v1T_4k&Yb&Bi-tlIy;Ni$K-tb=z|CgVG zfYNTB>}wisUt(ApguiT!y5+i&MMR}Yp!evjH#fcw+=P6!Vss%Hm`JPUec)<-y4!To z{&R2ht)@CL?ok`?rvziPkpdRDzEQlz5d`r9Kejoh0>yV{m#4%sb?T|pHR(Q{L>SOq z8iq;WEy#}vPT=}a$h}^KzIxdZ+1LZhk^^iL<3}Y?M4&yDZ zgN<({9RT97P*bDC(_(?V_D+=k*gj^_RNVCDwm&3yGD`+_gh!(`Xc59T+(%MJD9e>} zw+G#=iru5qEg!xlL-ckR^t4tlSu}t}B@@Fc`hL|6SjG+MUrxh@ux8M;g z3K11&erME|)}u}vR07{ffun~v{trTdJM0lb@XpSq22Tuaa%Q)i6#4)>IbFt(CoN24 zc5~{gi>TnII3{kbPJxzQIg(;_wd&a_#~;{(%EX?hf}x>g#)@@gdfxn-L_ zg~57kDdZ5vkQ9fvbj8;GP+?pat2(vs+gqgrySF2SZ+7Vkb9?xtC5F|bM}y@3Lt*~p z7R!OxjLA~y#Q?wg70^O+kSS*bWU36#`tZ?ttk@^cM6t s(?T<(wZ>agVu6BZ;ad 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 literal 0 HcmV?d00001 diff --git a/purchase_requisition_proposal/tests/__init__.py b/purchase_requisition_proposal/tests/__init__.py new file mode 100644 index 000000000..48da5a91c --- /dev/null +++ b/purchase_requisition_proposal/tests/__init__.py @@ -0,0 +1 @@ +from . import test_purchase_requirement_proposal diff --git a/purchase_requisition_proposal/tests/test_purchase_requirement_proposal.py b/purchase_requisition_proposal/tests/test_purchase_requirement_proposal.py new file mode 100644 index 000000000..4bac5a5dc --- /dev/null +++ b/purchase_requisition_proposal/tests/test_purchase_requirement_proposal.py @@ -0,0 +1,182 @@ +# Copyright 2023 Akretion (https://www.akretion.com). +# @author Kévin Roche +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo.addons.mail.tests.common import MockEmail +from odoo.addons.purchase_sale_inter_company.tests.test_inter_company_purchase_sale import ( + TestPurchaseSaleInterCompany, +) + + +class TestPurchaseRequirementProposal(MockEmail, TestPurchaseSaleInterCompany): + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.call_for_proposal_type = cls.env.ref( + "purchase_requisition_proposal.call_for_proposals_type" + ) + + cls.company_a.so_from_po = True + cls.company_a.warehouse_id = cls.warehouse_a + cls.company_a.requisition_intercompany_partner_ids = cls.user_company_a + cls.company_b.requisition_intercompany_partner_ids = cls.user_company_b + + cls.partner_company_a.email = "email_a.email@email.com" + cls.partner_company_b.email = "email_b.email@email.com" + + cls.product_uom_id = cls.env.ref("uom.product_uom_unit") + + cls.product_1 = cls.product.create({"name": "product_1"}) + cls.product_2 = cls.product.create({"name": "product_2"}) + + cls.call_for_proposal_type.multi_company_selection = "all" + + cls.line1 = ( + 0, + 0, + { + "product_id": cls.product_1.id, + "product_qty": 10, + "product_uom_id": cls.product_uom_id.id, + "price_unit": 100, + }, + ) + cls.line2 = ( + 0, + 0, + { + "product_id": cls.product_2.id, + "product_qty": 20, + "product_uom_id": cls.product_uom_id.id, + "price_unit": 200, + }, + ) + + cls.call = cls.env["purchase.requisition"].create( + { + "type_id": cls.call_for_proposal_type.id, + "date_end": "2222-06-15", + "schedule_date": "2222-06-21", + "line_ids": [cls.line1, cls.line2], + } + ) + + cls.call._onchange_schedule_date() + cls.call_line1 = cls.call.line_ids[0] + cls.call_line2 = cls.call.line_ids[1] + + cls.call.action_in_progress() + cls.call_line1.with_user(cls.user_company_a).create_proposal() + cls.proposal_1 = cls.call_line1.proposal_line_ids[0] + + def test_1_call_for_proposal_type(self): + self.assertEqual(len(self.call.company_to_call_ids), 3) + self.call_for_proposal_type.multi_company_selection = "selected" + self.call_for_proposal_type.company_to_call_ids = self.company_b + self.call2 = self.env["purchase.requisition"].create( + { + "type_id": self.call_for_proposal_type.id, + "date_end": "2222-06-15", + "schedule_date": "2222-06-21", + "line_ids": [self.line1], + } + ) + self.assertEqual(len(self.call2.company_to_call_ids), 1) + + def test_2_call_for_proposal_settings(self): + self.assertEqual(self.call.schedule_date, self.call_line1.schedule_date) + self.assertTrue(self.call_line1.proposal_line_count, 1) + + def test_3_validate_call_and_send_mail(self): + email = self.call.action_call_for_proposal_send() + composer = ( + self.env["mail.compose.message"].with_context(email["context"]).create({}) + ) + with self.mock_mail_gateway(): + composer.action_send_mail() + mail = self.env["mail.mail"].search([("model", "=", "purchase.requisition")]) + self.assertTrue(mail) + # self.assertIn( + # "The Agreement Deadline is", mail.body + # ) + + def test_4_proposal_accept(self): + self.assertEqual(self.call_line1.proposal_line_count, 1) + self.assertEqual(self.call_line1.product_qty, self.proposal_1.qty_proposed) + self.assertEqual(self.call_line1.schedule_date, self.proposal_1.date_planned) + self.assertEqual(self.company_a.partner_id, self.proposal_1.partner_id) + self.call_line1.with_user(self.user_company_b).create_proposal() + self.assertEqual(self.call_line1.proposal_line_count, 2) + + def test_5_proposal_duplicate_and_remove(self): + self.proposal_1.duplicate_line() + self.assertEqual(len(self.call_line1.proposal_line_ids), 2) + self.proposal_1.remove_line() + self.assertEqual(len(self.call_line1.proposal_line_ids), 1) + + def test_6_proposal_check_line(self): + self.assertEqual(self.proposal_1.date_check, "respected") + self.assertEqual(self.proposal_1.qty_check, "not_enough") + self.proposal_1.date_planned = "2222-06-23" + self.assertEqual(self.proposal_1.date_check, "not_respected") + self.proposal_1.qty_planned = 10 + self.assertEqual(self.proposal_1.qty_check, "enough") + + def test_7_proposal_to_call(self): + self.assertEqual(self.call_line1.date_check, "none") + self.assertEqual(self.call_line1.qty_check, "not_enough") + self.proposal_1.qty_planned = 10 + self.proposal_1.date_planned = "2222-06-20" + self.assertEqual(self.call_line1.date_check, "respected") + self.assertEqual(self.call_line1.qty_check, "enough") + self.proposal_1.date_planned = "2222-06-23" + self.assertEqual(self.call_line1.date_check, "not_respected") + + def test_8_create_rfq(self): + self.call.action_call_for_proposal_send() + self.call_line2.with_company(self.company_b).create_proposal() + self.call.action_create_quotations() + self.assertFalse(self.call.purchase_ids) + self.proposal_1.qty_planned = 10 + self.call_line2.proposal_line_ids[0].qty_planned = 30 + self.call.action_create_quotations() + self.assertEqual(len(self.call.purchase_ids), 2) + self.assertEqual(self.call.purchase_ids[0].partner_id, self.partner_company_a) + self.assertEqual(self.call.purchase_ids[1].partner_id, self.partner_company_b) + pol1, pol2 = self.call.purchase_ids.order_line + self.assertEqual(pol1.product_qty, self.call_line1.qty_planned) + self.assertEqual(pol2.product_qty, self.call_line2.qty_planned) + self.assertEqual(pol1.requirement_proposal_id, self.proposal_1) + self.assertEqual( + pol2.requirement_proposal_id, self.call_line2.proposal_line_ids[0] + ) + + self.call.purchase_ids.button_confirm() + self.assertEqual(self.call_line1.qty_ordered, self.call_line1.qty_planned) + self.assertEqual(self.call_line2.qty_ordered, self.call_line2.qty_planned) + + def test_9_related_sale(self): + self.call.action_call_for_proposal_send() + self.call_line2.with_company(self.company_b).create_proposal() + self.proposal_1.qty_planned = 10 + self.call_line2.proposal_line_ids[0].qty_planned = 20 + self.call.action_create_quotations() + self.call.purchase_ids[0].with_user(self.env.user).button_confirm() + self.call.purchase_ids[1].with_user(self.env.user).button_confirm() + so_a = ( + self.env["sale.order"] + .with_company(self.company_a) + .search([("purchase_requisition_id", "=", self.call.id)]) + ) + self.assertIn(self.proposal_1.id, so_a.order_line.requirement_proposal_id.ids) + so_b = ( + self.env["sale.order"] + .with_company(self.company_b) + .search([("purchase_requisition_id", "=", self.call.id)]) + ) + self.assertIn( + self.call_line2.proposal_line_ids[0].id, + so_b.order_line.requirement_proposal_id.ids, + ) + self.assertEqual(so_b.order_line.mapped("price_unit"),[200.0, 100.0]) diff --git a/purchase_requisition_proposal/views/ir_config.xml b/purchase_requisition_proposal/views/ir_config.xml new file mode 100644 index 000000000..e39372894 --- /dev/null +++ b/purchase_requisition_proposal/views/ir_config.xml @@ -0,0 +1,28 @@ + + + + res.config.settings + + + +
+
+
+
+
+
diff --git a/purchase_requisition_proposal/views/purchase_order.xml b/purchase_requisition_proposal/views/purchase_order.xml new file mode 100644 index 000000000..61082de65 --- /dev/null +++ b/purchase_requisition_proposal/views/purchase_order.xml @@ -0,0 +1,20 @@ + + + + + purchase.order.form + purchase.order + + + + + + + + diff --git a/purchase_requisition_proposal/views/purchase_requisition.xml b/purchase_requisition_proposal/views/purchase_requisition.xml new file mode 100644 index 000000000..6e88e5d3e --- /dev/null +++ b/purchase_requisition_proposal/views/purchase_requisition.xml @@ -0,0 +1,219 @@ + + + + + purchase.requisition.form + purchase.requisition + + + + {'requisitions_from': context.get('requisitions_from')} + + + {'invisible': ['|', ('state', '!=', 'open'), ('exclusive', '=', 'proposals')]} + + + {'invisible': ['|', ('state', 'not in', ('in_progress', 'ongoing')), ('exclusive', '=', 'proposals')]} + + + context.get('requisitions_from') != 'other' + + + + + + + + + qty_check == 'enough' and date_check == 'respected' + + + + + + purchase.requisition.form + purchase.requisition + + 1000 + primary + + + 0 + 0 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + + + + + + +