diff --git a/purchase_blanket_order/README.rst b/purchase_blanket_order/README.rst new file mode 100644 index 00000000000..da57e6e2c83 --- /dev/null +++ b/purchase_blanket_order/README.rst @@ -0,0 +1,136 @@ +======================= +Purchase Blanket Orders +======================= + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fpurchase--workflow-lightgray.png?logo=github + :target: https://github.com/OCA/purchase-workflow/tree/15.0/purchase_blanket_order + :alt: OCA/purchase-workflow +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/purchase-workflow-15-0/purchase-workflow-15-0-purchase_blanket_order + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/142/15.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +A purchase blanket order is a pre-agreement to purchase a certain number of +quantities of products at a specific price. From a confirmed blanket order, +the users can create new purchase orders at such price, until the blanket +order expires due to reaching the validity date. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +A new menu in the Purchase area is created, allowing users to create new blanket orders. + +To create a new Purchase Blanket Order go to the purchase menu in the Purchase section: + +.. image:: https://raw.githubusercontent.com/purchase_blanket_order/static/description/BO_menu.png + :alt: Blanket Orders menu + +Hitting the button create will open the form view in which we can introduce the following +information: + +* Vendor +* Payment Terms +* Ordering and Validity dates +* Order lines: + * Product + * Accorded price + * Original, Ordered, Invoiced, Received and Remaining quantities +* Terms and Conditions of the Blanket Order + +.. image:: https://raw.githubusercontent.com/purchase_blanket_order/static/description/BO_form.png + :alt: Blanket Orders form + +From the form, once the Blanket Order has been confirmed and its state is open, the user can +create a Purchase Order, check the Purchase Orders associated to the Blanket Order and/or +see the Blanket Order lines associated to the BO. + +.. image:: https://raw.githubusercontent.com/purchase_blanket_order/static/description/BO_actions.png + :alt: Actions that can be done from Blanket Order + +Hitting the button Create Purchase Order will open a wizard that will ask for the amount of each +product in the BO lines for which the Purchase Order will be created. + +.. image:: https://raw.githubusercontent.com/purchase_blanket_order/static/description/PO_from_BO.png + :alt: Create Purchase Order from Blanket Order + +Installing this module will add an additional menu which will show all the blanket order lines +currently defined in the system. From this list the user can create customized Purchase Orders +selecting the lines for which the PO (or POs if the vendors are different) is (are) created. + +.. image:: https://raw.githubusercontent.com/purchase_blanket_order/static/description/BO_lines.png + :alt: Blanket Order lines and actions + +In the Purchase Order form one field is added in the PO lines, the Blanket Order line field. This +field keeps track to which Blanket Order line the PO line is associated. Upon adding a new product +in a newly created Purchase Order a blanket order line will be suggested depending on the following +factors: + +* Closer Validity date +* Remaining quantity > Quantity introduced in the Purchase Order line + +.. image:: https://raw.githubusercontent.com/purchase_blanket_order/static/description/PO_BOLine.png + :alt: New field added in Purchase Order Line + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* ForgeFlow + +Contributors +~~~~~~~~~~~~ + +* Adrià Gil Sorribes +* Jordi Ballester Alomar +* Héctor Villarreal +* Lois Rilo +* Dhara Solanki + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/purchase-workflow `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/purchase_blanket_order/__init__.py b/purchase_blanket_order/__init__.py new file mode 100644 index 00000000000..93aa2c1f84b --- /dev/null +++ b/purchase_blanket_order/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import models +from . import wizard diff --git a/purchase_blanket_order/__manifest__.py b/purchase_blanket_order/__manifest__.py new file mode 100644 index 00000000000..fa81556e845 --- /dev/null +++ b/purchase_blanket_order/__manifest__.py @@ -0,0 +1,25 @@ +# Copyright 2019-22 ForgeFlow S.L. (https://www.forgeflow.com) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + "name": "Purchase Blanket Orders", + "category": "Purchase", + "license": "AGPL-3", + "author": "ForgeFlow, Odoo Community Association (OCA)", + "version": "16.0.1.0.0", + "website": "https://github.com/OCA/purchase-workflow", + "summary": "Purchase Blanket Orders", + "depends": ["purchase", "web_action_conditionable"], + "data": [ + "security/ir.model.access.csv", + "security/security.xml", + "data/sequence.xml", + "data/ir_cron.xml", + "wizard/create_purchase_orders.xml", + "views/purchase_config_settings.xml", + "views/purchase_blanket_order_views.xml", + "views/purchase_order_views.xml", + "report/templates.xml", + "report/report.xml", + ], + "installable": True, +} diff --git a/purchase_blanket_order/data/ir_cron.xml b/purchase_blanket_order/data/ir_cron.xml new file mode 100644 index 00000000000..01a96163279 --- /dev/null +++ b/purchase_blanket_order/data/ir_cron.xml @@ -0,0 +1,23 @@ + + + + + Expire Blanket Orders + 1 + days + + -1 + + + code + model.expire_orders() + + diff --git a/purchase_blanket_order/data/sequence.xml b/purchase_blanket_order/data/sequence.xml new file mode 100644 index 00000000000..6763c2e9a53 --- /dev/null +++ b/purchase_blanket_order/data/sequence.xml @@ -0,0 +1,11 @@ + + + + + Purchase Blanket Order + purchase.blanket.order + PBO + 3 + + + diff --git a/purchase_blanket_order/i18n/purchase_blanket_order.pot b/purchase_blanket_order/i18n/purchase_blanket_order.pot new file mode 100644 index 00000000000..c1e25fd5a35 --- /dev/null +++ b/purchase_blanket_order/i18n/purchase_blanket_order.pot @@ -0,0 +1,931 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * purchase_blanket_order +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0+e\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2023-05-29 10:57+0000\n" +"PO-Revision-Date: 2023-05-29 10:57+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_blanket_order +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.report_blanketorder_document +msgid "Blanket Order # " +msgstr "" + +#. module: purchase_blanket_order +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.report_blanketorder_document +msgid "Currency:" +msgstr "" + +#. module: purchase_blanket_order +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.report_blanketorder_document +msgid "Payment Terms:" +msgstr "" + +#. module: purchase_blanket_order +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.report_blanketorder_document +msgid "Purchase person:" +msgstr "" + +#. module: purchase_blanket_order +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.report_blanketorder_document +msgid "Subtotal" +msgstr "" + +#. module: purchase_blanket_order +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.report_blanketorder_document +msgid "Total" +msgstr "" + +#. module: purchase_blanket_order +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.report_blanketorder_document +msgid "Validity Date:" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__message_needaction +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__message_needaction +msgid "Action Needed" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__activity_ids +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__activity_ids +msgid "Activities" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__activity_exception_decoration +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__activity_exception_decoration +msgid "Activity Exception Decoration" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__activity_state +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__activity_state +msgid "Activity State" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__activity_type_icon +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__activity_type_icon +msgid "Activity Type Icon" +msgstr "" + +#. module: purchase_blanket_order +#. odoo-python +#: code:addons/purchase_blanket_order/wizard/create_purchase_orders.py:0 +#, python-format +msgid "All lines have already been completed." +msgstr "" + +#. module: purchase_blanket_order +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.report_blanketorder_document +msgid "Amount" +msgstr "" + +#. module: purchase_blanket_order +#. odoo-python +#: code:addons/purchase_blanket_order/wizard/create_purchase_orders.py:0 +#, python-format +msgid "An order can't be empty" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__message_attachment_count +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__message_attachment_count +msgid "Attachment Count" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_wizard_line__blanket_line_id +msgid "Blanket Line" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_wizard__blanket_order_id +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.view_purchase_blanket_order_form +msgid "Blanket Order" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_order_line__blanket_order_line +msgid "Blanket Order Line" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.actions.act_window,name:purchase_blanket_order.act_open_purchase_blanket_order_lines_view_tree +#: model:ir.ui.menu,name:purchase_blanket_order.menu_purchase_blanket_order_line +msgid "Blanket Order Lines" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model,name:purchase_blanket_order.model_purchase_blanket_order_wizard +msgid "Blanket Order Wizard" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,help:purchase_blanket_order.field_purchase_blanket_order__date_start +msgid "Blanket Order starting date." +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.actions.act_window,name:purchase_blanket_order.act_open_purchase_blanket_order_view +#: model:ir.ui.menu,name:purchase_blanket_order.menu_purchase_blanket_order_config +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.purchase_config_settings_form_view +msgid "Blanket Orders" +msgstr "" + +#. module: purchase_blanket_order +#. odoo-python +#: code:addons/purchase_blanket_order/wizard/create_purchase_orders.py:0 +#, python-format +msgid "" +"Can not create Purchase Order from Blanket Order lines with different " +"currencies" +msgstr "" + +#. module: purchase_blanket_order +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.view_create_purchase_order +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.view_purchase_blanket_order_form +msgid "Cancel" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__cancelled +msgid "Cancelled" +msgstr "" + +#. module: purchase_blanket_order +#. odoo-python +#: code:addons/purchase_blanket_order/models/purchase_order.py:0 +#, python-format +msgid "" +"Cannot confirm order %s as one of the lines refers to a blanket order that " +"has no remaining quantity." +msgstr "" + +#. module: purchase_blanket_order +#: model_terms:ir.actions.act_window,help:purchase_blanket_order.act_open_purchase_blanket_order_view +msgid "" +"Click to create a blanket order that can be converted into a purchase order." +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__company_id +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__company_id +msgid "Company" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model,name:purchase_blanket_order.model_res_config_settings +msgid "Config Settings" +msgstr "" + +#. module: purchase_blanket_order +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.view_purchase_blanket_order_form +msgid "Confirm" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__confirmed +msgid "Confirmed" +msgstr "" + +#. module: purchase_blanket_order +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.view_create_purchase_order +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.view_purchase_blanket_order_form +msgid "Create Purchase Order" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.actions.act_window,name:purchase_blanket_order.action_create_purchase_order +msgid "Create RFQ" +msgstr "" + +#. module: purchase_blanket_order +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.view_create_purchase_order +msgid "Create and View Order" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__create_uid +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__create_uid +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_wizard__create_uid +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_wizard_line__create_uid +msgid "Created by" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__create_date +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__create_date +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_wizard__create_date +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_wizard_line__create_date +msgid "Created on" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__currency_id +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__currency_id +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_wizard_line__currency_id +msgid "Currency" +msgstr "" + +#. module: purchase_blanket_order +#. odoo-python +#: code:addons/purchase_blanket_order/models/blanket_orders.py:0 +#, python-format +msgid "Date Scheduled" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,help:purchase_blanket_order.field_purchase_blanket_order__validity_date +msgid "" +"Date until which the blanket order will be valid, after this date the " +"blanket order will be marked as expired" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__name +msgid "Description" +msgstr "" + +#. module: purchase_blanket_order +#: model:res.groups,name:purchase_blanket_order.purchase_blanket_orders_disable_adding_lines +msgid "Disable adding more lines to POs from Purchase Blanket Orders" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_res_config_settings__group_purchase_blanket_disable_adding_lines +msgid "Disable adding more lines to SOs" +msgstr "" + +#. module: purchase_blanket_order +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.purchase_config_settings_form_view +msgid "Disable adding more lines to SOs from Blanket Orders" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__display_name +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__display_name +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_wizard__display_name +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_wizard_line__display_name +msgid "Display Name" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields.selection,name:purchase_blanket_order.selection__purchase_blanket_order__state__done +msgid "Done" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields.selection,name:purchase_blanket_order.selection__purchase_blanket_order__state__draft +msgid "Draft" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.actions.server,name:purchase_blanket_order.expired_purchase_blanket_orders_cron_ir_actions_server +#: model:ir.cron,cron_name:purchase_blanket_order.expired_purchase_blanket_orders_cron +msgid "Expire Blanket Orders" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields.selection,name:purchase_blanket_order.selection__purchase_blanket_order__state__expired +msgid "Expired" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__fiscal_position_id +msgid "Fiscal Position" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__message_follower_ids +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__message_follower_ids +msgid "Followers" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__message_partner_ids +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__message_partner_ids +msgid "Followers (Partners)" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,help:purchase_blanket_order.field_purchase_blanket_order__activity_type_icon +#: model:ir.model.fields,help:purchase_blanket_order.field_purchase_blanket_order_line__activity_type_icon +msgid "Font awesome icon e.g. fa-tasks" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__has_message +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__has_message +msgid "Has Message" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__id +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__id +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_wizard__id +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_wizard_line__id +msgid "ID" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__activity_exception_icon +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__activity_exception_icon +msgid "Icon" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,help:purchase_blanket_order.field_purchase_blanket_order__activity_exception_icon +#: model:ir.model.fields,help:purchase_blanket_order.field_purchase_blanket_order_line__activity_exception_icon +msgid "Icon to indicate an exception activity." +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,help:purchase_blanket_order.field_purchase_blanket_order__message_needaction +#: model:ir.model.fields,help:purchase_blanket_order.field_purchase_blanket_order_line__message_needaction +msgid "If checked, new messages require your attention." +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,help:purchase_blanket_order.field_purchase_blanket_order__message_has_error +#: model:ir.model.fields,help:purchase_blanket_order.field_purchase_blanket_order__message_has_sms_error +#: model:ir.model.fields,help:purchase_blanket_order.field_purchase_blanket_order_line__message_has_error +#: model:ir.model.fields,help:purchase_blanket_order.field_purchase_blanket_order_line__message_has_sms_error +msgid "If checked, some messages have a delivery error." +msgstr "" + +#. module: purchase_blanket_order +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.view_purchase_blanket_order_search +msgid "Invoiced Qty" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__invoiced_uom_qty +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__invoiced_uom_qty +msgid "Invoiced quantity" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__message_is_follower +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__message_is_follower +msgid "Is Follower" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order____last_update +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line____last_update +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_wizard____last_update +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_wizard_line____last_update +msgid "Last Modified on" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__write_uid +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__write_uid +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_wizard__write_uid +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_wizard_line__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__write_date +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__write_date +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_wizard__write_date +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_wizard_line__write_date +msgid "Last Updated on" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_wizard__line_ids +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.view_purchase_blanket_order_form +msgid "Lines" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__message_main_attachment_id +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__message_main_attachment_id +msgid "Main Attachment" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__message_has_error +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__message_has_error +msgid "Message Delivery error" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__message_ids +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__message_ids +msgid "Messages" +msgstr "" + +#. module: purchase_blanket_order +#. odoo-python +#: code:addons/purchase_blanket_order/models/blanket_orders.py:0 +#, python-format +msgid "Must have some lines" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__my_activity_date_deadline +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__my_activity_date_deadline +msgid "My Activity Deadline" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__name +msgid "Name" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__activity_date_deadline +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__activity_date_deadline +msgid "Next Activity Deadline" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__activity_summary +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__activity_summary +msgid "Next Activity Summary" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__activity_type_id +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__activity_type_id +msgid "Next Activity Type" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__note +msgid "Note" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__message_needaction_counter +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__message_needaction_counter +msgid "Number of Actions" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__message_has_error_counter +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__message_has_error_counter +msgid "Number of errors" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,help:purchase_blanket_order.field_purchase_blanket_order__message_needaction_counter +#: model:ir.model.fields,help:purchase_blanket_order.field_purchase_blanket_order_line__message_needaction_counter +msgid "Number of messages which requires an action" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,help:purchase_blanket_order.field_purchase_blanket_order__message_has_error_counter +#: model:ir.model.fields,help:purchase_blanket_order.field_purchase_blanket_order_line__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields.selection,name:purchase_blanket_order.selection__purchase_blanket_order__state__open +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.view_purchase_blanket_order_search +msgid "Open" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__order_id +msgid "Order" +msgstr "" + +#. module: purchase_blanket_order +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.view_purchase_blanket_order_form +msgid "Order Lines" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__line_ids +msgid "Order lines" +msgstr "" + +#. module: purchase_blanket_order +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.view_purchase_blanket_order_search +msgid "Ordered Qty" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__ordered_uom_qty +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__ordered_uom_qty +msgid "Ordered quantity" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_order__blanket_order_id +msgid "Origin blanket order" +msgstr "" + +#. module: purchase_blanket_order +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.purchase_blanket_order_line_tree +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.report_blanketorder_document +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.view_purchase_blanket_order_form +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.view_purchase_blanket_order_search +msgid "Original Qty" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__original_uom_qty +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__original_uom_qty +msgid "Original quantity" +msgstr "" + +#. module: purchase_blanket_order +#. odoo-python +#: code:addons/purchase_blanket_order/models/blanket_orders.py:0 +#, python-format +msgid "Partner is mandatory" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__payment_term_id +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__payment_term_id +msgid "Payment Terms" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__price_unit +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_wizard_line__price_unit +msgid "Price" +msgstr "" + +#. module: purchase_blanket_order +#. odoo-python +#: code:addons/purchase_blanket_order/models/blanket_orders.py:0 +#, python-format +msgid "Price must be greater than zero" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__product_id +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__product_id +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_wizard_line__product_id +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.report_blanketorder_document +msgid "Product" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.actions.report,name:purchase_blanket_order.report_blanket_order +#: model:ir.model,name:purchase_blanket_order.model_purchase_blanket_order +msgid "Purchase Blanket Order" +msgstr "" + +#. module: purchase_blanket_order +#. odoo-python +#: code:addons/purchase_blanket_order/wizard/create_purchase_orders.py:0 +#, python-format +msgid "Purchase Blanket Order %s is not open" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model,name:purchase_blanket_order.model_purchase_blanket_order_line +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.purchase_blanket_order_line_form +msgid "Purchase Blanket Order Line" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__line_count +msgid "Purchase Blanket Order Line count" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model,name:purchase_blanket_order.model_purchase_blanket_order_wizard_line +msgid "Purchase Blanket Order Wizard Line" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__purchase_count +msgid "Purchase Count" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model,name:purchase_blanket_order.model_purchase_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_wizard__purchase_order_id +msgid "Purchase Order" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model,name:purchase_blanket_order.model_purchase_order_line +msgid "Purchase Order Line" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__purchase_lines +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.purchase_blanket_order_line_form +msgid "Purchase Order Lines" +msgstr "" + +#. module: purchase_blanket_order +#. odoo-python +#: code:addons/purchase_blanket_order/models/blanket_orders.py:0 +#, python-format +msgid "Quantity must be greater than zero" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_wizard_line__qty +msgid "Quantity to Order" +msgstr "" + +#. module: purchase_blanket_order +#. odoo-python +#: code:addons/purchase_blanket_order/wizard/create_purchase_orders.py:0 +#, python-format +msgid "RFQ" +msgstr "" + +#. module: purchase_blanket_order +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.view_purchase_blanket_order_form +msgid "RFQ/Orders" +msgstr "" + +#. module: purchase_blanket_order +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.view_purchase_blanket_order_search +msgid "Received Qty" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__received_uom_qty +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__received_uom_qty +msgid "Received quantity" +msgstr "" + +#. module: purchase_blanket_order +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.view_purchase_blanket_order_search +msgid "Remaining Qty" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__remaining_uom_qty +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__remaining_uom_qty +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_wizard_line__remaining_uom_qty +msgid "Remaining quantity" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__remaining_qty +msgid "Remaining quantity in base UoM" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__user_id +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__user_id +msgid "Responsible" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__activity_user_id +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__activity_user_id +msgid "Responsible User" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__message_has_sms_error +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__message_has_sms_error +msgid "SMS Delivery error" +msgstr "" + +#. module: purchase_blanket_order +#. odoo-python +#: code:addons/purchase_blanket_order/models/purchase_order.py:0 +#, python-format +msgid "" +"Schedule dates defined on the Purchase Order Line and on the Blanket Order " +"Line do not match." +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__date_schedule +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_wizard_line__date_schedule +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.report_blanketorder_document +msgid "Scheduled Date" +msgstr "" + +#. module: purchase_blanket_order +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.purchase_blanket_order_line_search +msgid "Search Purchase Blanket Order Line" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__sequence +msgid "Sequence" +msgstr "" + +#. module: purchase_blanket_order +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.view_purchase_blanket_order_form +msgid "Setup default terms and conditions in your company settings." +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__date_start +msgid "Start Date" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__state +msgid "State" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,help:purchase_blanket_order.field_purchase_blanket_order__activity_state +#: model:ir.model.fields,help:purchase_blanket_order.field_purchase_blanket_order_line__activity_state +msgid "" +"Status based on activities\n" +"Overdue: Due date is already passed\n" +"Today: Activity date is today\n" +"Planned: Future activities." +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__price_subtotal +msgid "Subtotal" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__price_tax +msgid "Tax" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__amount_tax +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__taxes_id +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_wizard_line__taxes_id +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.report_blanketorder_document +msgid "Taxes" +msgstr "" + +#. module: purchase_blanket_order +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.view_purchase_blanket_order_form +msgid "Terms and Conditions" +msgstr "" + +#. module: purchase_blanket_order +#. odoo-python +#: code:addons/purchase_blanket_order/models/purchase_order.py:0 +#: code:addons/purchase_blanket_order/models/purchase_order.py:0 +#, python-format +msgid "" +"The currency of the blanket order must match with that of the purchase " +"order." +msgstr "" + +#. module: purchase_blanket_order +#. odoo-python +#: code:addons/purchase_blanket_order/models/purchase_order.py:0 +#, python-format +msgid "The vendor must be equal to the blanket order lines vendor" +msgstr "" + +#. module: purchase_blanket_order +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.view_purchase_blanket_order_form +msgid "To Draft" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__amount_total +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__price_total +msgid "Total" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,help:purchase_blanket_order.field_purchase_blanket_order__activity_exception_decoration +#: model:ir.model.fields,help:purchase_blanket_order.field_purchase_blanket_order_line__activity_exception_decoration +msgid "Type of the exception activity on record." +msgstr "" + +#. module: purchase_blanket_order +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.report_blanketorder_document +msgid "Unit Price" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__product_uom +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_wizard_line__product_uom +msgid "Unit of Measure" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__amount_untaxed +msgid "Untaxed Amount" +msgstr "" + +#. module: purchase_blanket_order +#: model_terms:ir.actions.act_window,help:purchase_blanket_order.act_open_purchase_blanket_order_view +msgid "" +"Use this menu to search within your blanket orders. For each blanket order,\n" +" you can track the related discussion with the vendor, control\n" +" the products received and control the vendor bills." +msgstr "" + +#. module: purchase_blanket_order +#: model_terms:ir.ui.view,arch_db:purchase_blanket_order.report_blanketorder_document +msgid "VAT:" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__validity_date +msgid "Validity Date" +msgstr "" + +#. module: purchase_blanket_order +#. odoo-python +#: code:addons/purchase_blanket_order/models/blanket_orders.py:0 +#, python-format +msgid "Validity date is mandatory" +msgstr "" + +#. module: purchase_blanket_order +#. odoo-python +#: code:addons/purchase_blanket_order/models/blanket_orders.py:0 +#, python-format +msgid "Validity date must be in the future" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__partner_id +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__partner_id +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_wizard_line__partner_id +msgid "Vendor" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__partner_ref +msgid "Vendor Reference" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order__website_message_ids +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_line__website_message_ids +msgid "Website Messages" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,help:purchase_blanket_order.field_purchase_blanket_order__website_message_ids +#: model:ir.model.fields,help:purchase_blanket_order.field_purchase_blanket_order_line__website_message_ids +msgid "Website communication history" +msgstr "" + +#. module: purchase_blanket_order +#: model:ir.model.fields,field_description:purchase_blanket_order.field_purchase_blanket_order_wizard_line__wizard_id +msgid "Wizard" +msgstr "" + +#. module: purchase_blanket_order +#. odoo-python +#: code:addons/purchase_blanket_order/models/blanket_orders.py:0 +#, python-format +msgid "" +"You can not delete a blanket order with opened purchase orders! Try to " +"cancel them before." +msgstr "" + +#. module: purchase_blanket_order +#. odoo-python +#: code:addons/purchase_blanket_order/models/blanket_orders.py:0 +#, python-format +msgid "You can not delete an open blanket order! Try to cancel it before." +msgstr "" + +#. module: purchase_blanket_order +#. odoo-python +#: code:addons/purchase_blanket_order/wizard/create_purchase_orders.py:0 +#, python-format +msgid "You can't create a purchase order from an expired blanket order!" +msgstr "" + +#. module: purchase_blanket_order +#. odoo-python +#: code:addons/purchase_blanket_order/wizard/create_purchase_orders.py:0 +#, python-format +msgid "You can't order more than the remaining quantities" +msgstr "" + +#. module: purchase_blanket_order +#. odoo-python +#: code:addons/purchase_blanket_order/wizard/create_purchase_orders.py:0 +#, python-format +msgid "You have to select lines from the same company." +msgstr "" + +#. module: purchase_blanket_order +#. odoo-python +#: code:addons/purchase_blanket_order/models/blanket_orders.py:0 +#, python-format +msgid "remaining" +msgstr "" diff --git a/purchase_blanket_order/models/__init__.py b/purchase_blanket_order/models/__init__.py new file mode 100644 index 00000000000..a1217ef766a --- /dev/null +++ b/purchase_blanket_order/models/__init__.py @@ -0,0 +1,5 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import blanket_orders +from . import purchase_order +from . import purchase_config_settings diff --git a/purchase_blanket_order/models/blanket_orders.py b/purchase_blanket_order/models/blanket_orders.py new file mode 100644 index 00000000000..12d8103dfd6 --- /dev/null +++ b/purchase_blanket_order/models/blanket_orders.py @@ -0,0 +1,622 @@ +# Copyright (C) 2018 ForgeFlow S.L. (https://www.forgeflow.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from datetime import datetime + +from odoo import SUPERUSER_ID, _, api, fields, models +from odoo.exceptions import UserError +from odoo.tools import float_is_zero + + +class BlanketOrder(models.Model): + _name = "purchase.blanket.order" + _inherit = ["mail.thread", "mail.activity.mixin"] + _description = "Purchase Blanket Order" + _order = "date_start desc, id desc" + + @api.model + def _default_currency(self): + return self.env.user.company_id.currency_id + + @api.model + def _default_company(self): + return self.env.user.company_id + + @api.depends("line_ids.price_total") + def _compute_amount_all(self): + for order in self: + amount_untaxed = amount_tax = 0.0 + for line in order.line_ids: + amount_untaxed += line.price_subtotal + amount_tax += line.price_tax + order.update( + { + "amount_untaxed": order.currency_id.round(amount_untaxed), + "amount_tax": order.currency_id.round(amount_tax), + "amount_total": amount_untaxed + amount_tax, + } + ) + + name = fields.Char(default="Draft", readonly=True) + partner_id = fields.Many2one( + "res.partner", + string="Vendor", + readonly=True, + tracking=True, + states={"draft": [("readonly", False)]}, + ) + partner_ref = fields.Char(string="Vendor Reference", copy=False) + line_ids = fields.One2many( + "purchase.blanket.order.line", + "order_id", + string="Order lines", + tracking=True, + copy=True, + ) + line_count = fields.Integer( + string="Purchase Blanket Order Line count", + compute="_compute_line_count", + readonly=True, + ) + product_id = fields.Many2one( + "product.product", + related="line_ids.product_id", + string="Product", + ) + currency_id = fields.Many2one( + "res.currency", + required=True, + default=lambda self: self.env.user.company_id.currency_id.id, + ) + payment_term_id = fields.Many2one( + "account.payment.term", + string="Payment Terms", + readonly=True, + states={"draft": [("readonly", False)]}, + ) + confirmed = fields.Boolean(copy=False) + cancelled = fields.Boolean(copy=False) + state = fields.Selection( + selection=[ + ("draft", "Draft"), + ("open", "Open"), + ("done", "Done"), + ("expired", "Expired"), + ], + compute="_compute_state", + store=True, + copy=False, + tracking=True, + ) + validity_date = fields.Date( + readonly=True, + states={"draft": [("readonly", False)]}, + tracking=True, + help="Date until which the blanket order will be valid, after this " + "date the blanket order will be marked as expired", + ) + date_start = fields.Datetime( + readonly=True, + required=True, + string="Start Date", + default=fields.Datetime.now, + states={"draft": [("readonly", False)]}, + help="Blanket Order starting date.", + ) + note = fields.Text(readonly=True, states={"draft": [("readonly", False)]}) + user_id = fields.Many2one( + "res.users", + string="Responsible", + readonly=True, + default=lambda self: self.env.uid, + states={"draft": [("readonly", False)]}, + ) + company_id = fields.Many2one( + "res.company", + string="Company", + default=_default_company, + readonly=True, + states={"draft": [("readonly", False)]}, + ) + purchase_count = fields.Integer(compute="_compute_purchase_count") + + fiscal_position_id = fields.Many2one( + "account.fiscal.position", string="Fiscal Position" + ) + + amount_untaxed = fields.Monetary( + string="Untaxed Amount", + store=True, + readonly=True, + compute="_compute_amount_all", + tracking=True, + ) + amount_tax = fields.Monetary( + string="Taxes", store=True, readonly=True, compute="_compute_amount_all" + ) + amount_total = fields.Monetary( + string="Total", store=True, readonly=True, compute="_compute_amount_all" + ) + + # Fields use to filter in tree view + original_uom_qty = fields.Float( + string="Original quantity", + compute="_compute_uom_qty", + search="_search_original_uom_qty", + ) + ordered_uom_qty = fields.Float( + string="Ordered quantity", + compute="_compute_uom_qty", + search="_search_ordered_uom_qty", + ) + invoiced_uom_qty = fields.Float( + string="Invoiced quantity", + compute="_compute_uom_qty", + search="_search_invoiced_uom_qty", + ) + remaining_uom_qty = fields.Float( + string="Remaining quantity", + compute="_compute_uom_qty", + search="_search_remaining_uom_qty", + ) + received_uom_qty = fields.Float( + string="Received quantity", + compute="_compute_uom_qty", + search="_search_received_uom_qty", + ) + + def _get_purchase_orders(self): + return self.mapped("line_ids.purchase_lines.order_id") + + @api.depends("line_ids") + def _compute_line_count(self): + self.line_count = len(self.mapped("line_ids")) + + def _compute_purchase_count(self): + for blanket_order in self: + blanket_order.purchase_count = len(blanket_order._get_purchase_orders()) + + @api.depends( + "line_ids.remaining_uom_qty", + "validity_date", + "confirmed", + "cancelled", + ) + def _compute_state(self): + today = fields.Date.today() + precision = self.env["decimal.precision"].precision_get( + "Product Unit of Measure" + ) + for order in self: + if not order.confirmed and not order.cancelled: + order.state = "draft" + elif order.validity_date <= today or order.cancelled: + order.state = "expired" + elif float_is_zero( + sum(order.line_ids.mapped("remaining_uom_qty")), + precision_digits=precision, + ): + order.state = "done" + else: + order.state = "open" + + def _compute_uom_qty(self): + for bo in self: + bo.original_uom_qty = sum(bo.mapped("line_ids.original_uom_qty")) + bo.ordered_uom_qty = sum(bo.mapped("line_ids.ordered_uom_qty")) + bo.invoiced_uom_qty = sum(bo.mapped("line_ids.invoiced_uom_qty")) + bo.received_uom_qty = sum(bo.mapped("line_ids.received_uom_qty")) + bo.remaining_uom_qty = sum(bo.mapped("line_ids.remaining_uom_qty")) + + @api.onchange("partner_id") + def onchange_partner_id(self): + """ + Update the following fields when the partner is changed: + - Payment term + """ + if not self.partner_id: + self.payment_term_id = False + self.fiscal_position_id = False + return + + self.payment_term_id = ( + self.partner_id.property_supplier_payment_term_id + and self.partner_id.property_supplier_payment_term_id.id + or False + ) + + self.fiscal_position_id = ( + self.env["account.fiscal.position"] + .with_context(company_id=self.company_id.id) + ._get_fiscal_position(self.partner_id) + ) + + self.currency_id = ( + self.partner_id.property_purchase_currency_id.id + or self.env.user.company_id.currency_id.id + ) + + if self.partner_id.user_id: + self.user_id = self.partner_id.user_id.id + + def unlink(self): + for order in self: + if order.state not in ("draft", "expired"): + raise UserError( + _( + "You can not delete an open blanket order! " + "Try to cancel it before." + ) + ) + return super().unlink() + + def copy_data(self, default=None): + if default is None: + default = {} + default.update(self.default_get(["name", "confirmed"])) + return super().copy_data(default) + + def _validate(self): + try: + today = fields.Date.today() + for order in self: + assert order.validity_date, _("Validity date is mandatory") + assert order.validity_date > today, _( + "Validity date must be in the future" + ) + assert order.partner_id, _("Partner is mandatory") + assert len(order.line_ids) > 0, _("Must have some lines") + order.line_ids._validate() + except AssertionError as e: + raise UserError(e) from e + + def set_to_draft(self): + for order in self: + order.write({"cancelled": False}) + return True + + def action_confirm(self): + self._validate() + for order in self: + vals = {"confirmed": True} + # Set name by sequence only if is necessary + if order.name == "Draft": + sequence_obj = self.env["ir.sequence"] + if order.company_id: + sequence_obj = sequence_obj.with_company(order.company_id) + name = sequence_obj.next_by_code("purchase.blanket.order") or "Draft" + vals.update({"name": name}) + order.write(vals) + return True + + def action_cancel(self): + for order in self: + if order.purchase_count > 0: + for po in order._get_purchase_orders(): + if po.state not in ("cancel"): + raise UserError( + _( + "You can not delete a blanket order with opened " + "purchase orders! " + "Try to cancel them before." + ) + ) + order.write({"cancelled": True, "confirmed": False}) + return True + + def action_view_purchase_orders(self): + purchase_orders = self._get_purchase_orders() + action = self.env["ir.actions.actions"]._for_xml_id("purchase.purchase_rfq") + if len(purchase_orders) > 0: + action["domain"] = [("id", "in", purchase_orders.ids)] + action["context"] = [("id", "in", purchase_orders.ids)] + else: + action = {"type": "ir.actions.act_window_close"} + return action + + def action_view_purchase_blanket_order_line(self): + action = self.env["ir.actions.actions"]._for_xml_id( + "purchase_blanket_order.act_open_purchase_blanket_order_lines_view_tree" + ) + lines = self.mapped("line_ids") + if len(lines) > 0: + action["domain"] = [("id", "in", lines.ids)] + return action + + @api.model + def expire_orders(self): + today = fields.Date.today() + expired_orders = self.search( + [("state", "=", "open"), ("validity_date", "<=", today)] + ) + expired_orders.modified(["validity_date"]) + expired_orders.env.flush_all() + + @api.model + def _search_original_uom_qty(self, operator, value): + bo_line_obj = self.env["purchase.blanket.order.line"] + res = [] + bo_lines = bo_line_obj.search([("original_uom_qty", operator, value)]) + order_ids = bo_lines.mapped("order_id") + res.append(("id", "in", order_ids.ids)) + return res + + @api.model + def _search_ordered_uom_qty(self, operator, value): + bo_line_obj = self.env["purchase.blanket.order.line"] + res = [] + bo_lines = bo_line_obj.search([("ordered_uom_qty", operator, value)]) + order_ids = bo_lines.mapped("order_id") + res.append(("id", "in", order_ids.ids)) + return res + + @api.model + def _search_invoiced_uom_qty(self, operator, value): + bo_line_obj = self.env["purchase.blanket.order.line"] + res = [] + bo_lines = bo_line_obj.search([("invoiced_uom_qty", operator, value)]) + order_ids = bo_lines.mapped("order_id") + res.append(("id", "in", order_ids.ids)) + return res + + @api.model + def _search_received_uom_qty(self, operator, value): + bo_line_obj = self.env["purchase.blanket.order.line"] + res = [] + bo_lines = bo_line_obj.search([("received_uom_qty", operator, value)]) + order_ids = bo_lines.mapped("order_id") + res.append(("id", "in", order_ids.ids)) + return res + + @api.model + def _search_remaining_uom_qty(self, operator, value): + bo_line_obj = self.env["purchase.blanket.order.line"] + res = [] + bo_lines = bo_line_obj.search([("remaining_uom_qty", operator, value)]) + order_ids = bo_lines.mapped("order_id") + res.append(("id", "in", order_ids.ids)) + return res + + +class BlanketOrderLine(models.Model): + _name = "purchase.blanket.order.line" + _description = "Purchase Blanket Order Line" + _inherit = ["mail.thread", "mail.activity.mixin"] + + @api.depends("original_uom_qty", "price_unit", "taxes_id") + def _compute_amount(self): + for line in self: + taxes = line.taxes_id.compute_all( + line.price_unit, + line.order_id.currency_id, + line.original_uom_qty, + product=line.product_id, + partner=line.order_id.partner_id, + ) + line.update( + { + "price_tax": sum( + t.get("amount", 0.0) for t in taxes.get("taxes", []) + ), + "price_total": taxes["total_included"], + "price_subtotal": taxes["total_excluded"], + } + ) + + name = fields.Char(string="Description", tracking=True) + sequence = fields.Integer() + order_id = fields.Many2one( + "purchase.blanket.order", required=True, ondelete="cascade" + ) + product_id = fields.Many2one( + "product.product", + string="Product", + required=True, + domain=[("purchase_ok", "=", True)], + ) + product_uom = fields.Many2one("uom.uom", string="Unit of Measure", required=True) + price_unit = fields.Float(string="Price", required=True, digits=("Product Price")) + taxes_id = fields.Many2many( + "account.tax", + string="Taxes", + domain=["|", ("active", "=", False), ("active", "=", True)], + ) + date_schedule = fields.Date(string="Scheduled Date") + original_uom_qty = fields.Float( + string="Original quantity", + required=True, + default=1.0, + digits=("Product Unit of Measure"), + ) + ordered_uom_qty = fields.Float( + string="Ordered quantity", + compute="_compute_quantities", + store=True, + digits=("Product Unit of Measure"), + ) + invoiced_uom_qty = fields.Float( + string="Invoiced quantity", + compute="_compute_quantities", + store=True, + digits=("Product Unit of Measure"), + ) + remaining_uom_qty = fields.Float( + string="Remaining quantity", + compute="_compute_quantities", + store=True, + digits=("Product Unit of Measure"), + ) + remaining_qty = fields.Float( + string="Remaining quantity in base UoM", + compute="_compute_quantities", + store=True, + digits=("Product Unit of Measure"), + ) + received_uom_qty = fields.Float( + string="Received quantity", + compute="_compute_quantities", + store=True, + digits=("Product Unit of Measure"), + ) + purchase_lines = fields.One2many( + comodel_name="purchase.order.line", + inverse_name="blanket_order_line", + string="Purchase Order Lines", + readonly=True, + copy=False, + ) + company_id = fields.Many2one( + "res.company", related="order_id.company_id", store=True, readonly=True + ) + currency_id = fields.Many2one( + "res.currency", related="order_id.currency_id", readonly=True + ) + partner_id = fields.Many2one( + related="order_id.partner_id", string="Vendor", readonly=True + ) + user_id = fields.Many2one( + related="order_id.user_id", string="Responsible", readonly=True + ) + payment_term_id = fields.Many2one( + related="order_id.payment_term_id", string="Payment Terms", readonly=True + ) + + price_subtotal = fields.Monetary( + compute="_compute_amount", string="Subtotal", store=True + ) + price_total = fields.Monetary(compute="_compute_amount", string="Total", store=True) + price_tax = fields.Float(compute="_compute_amount", string="Tax", store=True) + + def _format_date(self, date): + # format date following user language + lang_model = self.env["res.lang"] + lang = lang_model._lang_get(self.env.user.lang) + date_format = lang.date_format + return datetime.strftime(fields.Date.from_string(date), date_format) + + def name_get(self): + result = [] + if self.env.context.get("from_purchase_order"): + for record in self: + res = "[%s]" % record.order_id.name + if record.date_schedule: + formatted_date = self._format_date(record.date_schedule) + res += " - {}: {}".format(_("Date Scheduled"), formatted_date) + res += " ({}: {} {})".format( + _("remaining"), + record.remaining_uom_qty, + record.product_uom.name, + ) + result.append((record.id, res)) + return result + return super().name_get() + + def _get_display_price(self, product): + + seller = product._select_seller( + partner_id=self.order_id.partner_id, + quantity=self.original_uom_qty, + date=self.order_id.date_start + and fields.Date.from_string(self.order_id.date_start), + uom_id=self.product_uom, + ) + + if not seller: + return + + price_unit = ( + self.env["account.tax"]._fix_tax_included_price_company( + seller.price, + product.supplier_taxes_id, + self.purchase_lines.taxes_id, + self.company_id, + ) + if seller + else 0.0 + ) + if ( + price_unit + and seller + and self.order_id.currency_id + and seller.currency_id != self.order_id.currency_id + ): + price_unit = seller.currency_id.compute( + price_unit, self.order_id.currency_id + ) + + if seller and self.product_uom and seller.product_uom != self.product_uom: + price_unit = seller.product_uom._compute_price(price_unit, self.product_uom) + + return price_unit + + @api.onchange("product_id", "original_uom_qty") + def onchange_product(self): + precision = self.env["decimal.precision"].precision_get( + "Product Unit of Measure" + ) + if self.product_id: + name = self.product_id.name + if not self.product_uom: + self.product_uom = self.product_id.uom_po_id or self.product_id.uom_id + if self.order_id.partner_id and float_is_zero( + self.price_unit, precision_digits=precision + ): + self.price_unit = self._get_display_price(self.product_id) + if self.product_id.code: + name = "[{}] {}".format(name, self.product_id.code) + if self.product_id.description_purchase: + name += "\n" + self.product_id.description_purchase + self.name = name + + fpos = self.order_id.fiscal_position_id + if self.env.uid == SUPERUSER_ID: + company_id = self.env.user.company_id.id + self.taxes_id = fpos.map_tax( + self.product_id.supplier_taxes_id.filtered( + lambda r: r.company_id.id == company_id + ) + ) + else: + self.taxes_id = fpos.map_tax(self.product_id.supplier_taxes_id) + + @api.depends( + "purchase_lines.order_id.state", + "purchase_lines.blanket_order_line", + "purchase_lines.product_qty", + "purchase_lines.product_uom", + "purchase_lines.qty_received", + "purchase_lines.qty_invoiced", + "original_uom_qty", + "product_uom", + ) + def _compute_quantities(self): + for line in self: + purchase_lines = line.purchase_lines + line.ordered_uom_qty = sum( + pol.product_uom._compute_quantity(pol.product_qty, line.product_uom) + for pol in purchase_lines + if pol.order_id.state != "cancel" and pol.product_id == line.product_id + ) + line.invoiced_uom_qty = sum( + pol.product_uom._compute_quantity(pol.qty_invoiced, line.product_uom) + for pol in purchase_lines + if pol.order_id.state != "cancel" and pol.product_id == line.product_id + ) + line.received_uom_qty = sum( + pol.product_uom._compute_quantity(pol.qty_received, line.product_uom) + for pol in purchase_lines + if pol.order_id.state != "cancel" and pol.product_id == line.product_id + ) + line.remaining_uom_qty = line.original_uom_qty - line.ordered_uom_qty + line.remaining_qty = line.product_uom._compute_quantity( + line.remaining_uom_qty, line.product_id.uom_id + ) + + def _validate(self): + try: + for line in self: + assert line.price_unit > 0.0, _("Price must be greater than zero") + assert line.original_uom_qty > 0.0, _( + "Quantity must be greater than zero" + ) + except AssertionError as e: + raise UserError(e) from e diff --git a/purchase_blanket_order/models/purchase_config_settings.py b/purchase_blanket_order/models/purchase_config_settings.py new file mode 100644 index 00000000000..30c695fe6ff --- /dev/null +++ b/purchase_blanket_order/models/purchase_config_settings.py @@ -0,0 +1,14 @@ +# Copyright (C) 2018 ForgeFlow S.L. (https://www.forgeflow.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class PurchaseConfigSettings(models.TransientModel): + _inherit = "res.config.settings" + + group_purchase_blanket_disable_adding_lines = fields.Boolean( + string="Disable adding more lines to SOs", + implied_group="purchase_blanket_order." + "purchase_blanket_orders_disable_adding_lines", + ) diff --git a/purchase_blanket_order/models/purchase_order.py b/purchase_blanket_order/models/purchase_order.py new file mode 100644 index 00000000000..a70a6328e4a --- /dev/null +++ b/purchase_blanket_order/models/purchase_order.py @@ -0,0 +1,180 @@ +# Copyright (C) 2018 ForgeFlow S.L. (https://www.forgeflow.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from datetime import date, timedelta + +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class PurchaseOrder(models.Model): + _inherit = "purchase.order" + + blanket_order_id = fields.Many2one( + "purchase.blanket.order", + string="Origin blanket order", + related="order_line.blanket_order_line.order_id", + readonly=True, + ) + + @api.model + def _check_exchausted_blanket_order_line(self): + return any( + line.blanket_order_line.remaining_qty < 0.0 for line in self.order_line + ) + + def button_confirm(self): + res = super().button_confirm() + for order in self: + if order._check_exchausted_blanket_order_line(): + raise ValidationError( + _( + "Cannot confirm order %s as one of the lines refers " + "to a blanket order that has no remaining quantity." + ) + % order.name + ) + return res + + @api.constrains("partner_id") + def check_partner_id(self): + for line in self.order_line: + if line.blanket_order_line: + if line.blanket_order_line.partner_id != self.partner_id: + raise ValidationError( + _( + "The vendor must be equal to the blanket order" + " lines vendor" + ) + ) + + @api.constrains("currency_id") + def check_currency(self): + for rec in self: + if any( + line.blanket_order_line.order_id.currency_id != rec.currency_id + for line in rec.order_line.filtered(lambda x: x.blanket_order_line) + ): + raise ValidationError( + _( + "The currency of the blanket order must match with that " + "of the purchase order." + ) + ) + + +class PurchaseOrderLine(models.Model): + _inherit = "purchase.order.line" + + blanket_order_line = fields.Many2one( + comodel_name="purchase.blanket.order.line", copy=False + ) + + def _get_assigned_bo_line(self, bo_lines): + # We get the blanket order line with enough quantity and closest + # scheduled date + assigned_bo_line = False + date_planned = fields.Date.from_string(self.date_planned) or date.today() + date_delta = timedelta(days=365) + for line in bo_lines.filtered(lambda l: l.date_schedule): + date_schedule = fields.Date.from_string(line.date_schedule) + if date_schedule and abs(date_schedule - date_planned) < date_delta: + assigned_bo_line = line + date_delta = abs(date_schedule - date_planned) + if assigned_bo_line: + return assigned_bo_line + non_date_bo_lines = bo_lines.filtered(lambda l: not l.date_schedule) + if non_date_bo_lines: + return non_date_bo_lines[0] + + def _get_eligible_bo_lines_domain(self, base_qty): + filters = [ + ("product_id", "=", self.product_id.id), + ("remaining_qty", ">=", base_qty), + ("currency_id", "=", self.order_id.currency_id.id), + ("order_id.state", "=", "open"), + ] + if self.order_id.partner_id: + filters.append(("partner_id", "=", self.order_id.partner_id.id)) + return filters + + def _get_eligible_bo_lines(self): + base_qty = self.product_uom._compute_quantity( + self.product_qty, self.product_id.uom_id + ) + filters = self._get_eligible_bo_lines_domain(base_qty) + return self.env["purchase.blanket.order.line"].search(filters) + + def get_assigned_bo_line(self): + self.ensure_one() + eligible_bo_lines = self._get_eligible_bo_lines() + if eligible_bo_lines: + if ( + not self.blanket_order_line + or self.blanket_order_line not in eligible_bo_lines + ): + self.blanket_order_line = self._get_assigned_bo_line(eligible_bo_lines) + else: + self.blanket_order_line = False + self.onchange_blanket_order_line() + return {"domain": {"blanket_order_line": [("id", "in", eligible_bo_lines.ids)]}} + + @api.onchange("product_id", "partner_id") + def onchange_product_id(self): + res = super().onchange_product_id() + # If product has changed remove the relation with blanket order line + if self.product_id: + return self.get_assigned_bo_line() + return res + + @api.onchange("product_qty", "product_uom") + def _onchange_quantity(self): + if self.product_id and not self.env.context.get("skip_blanket_find", False): + return self.get_assigned_bo_line() + + @api.onchange("blanket_order_line") + def onchange_blanket_order_line(self): + bol = self.blanket_order_line + if bol: + if bol.date_schedule: + self.date_planned = bol.date_schedule + self.product_id = bol.product_id + if bol.product_uom != self.product_uom: + price_unit = bol.product_uom._compute_price( + bol.price_unit, self.product_uom + ) + else: + price_unit = bol.price_unit + self.price_unit = price_unit + if bol.taxes_id: + self.taxes_id = bol.taxes_id + else: + self._compute_tax_id() + self.with_context(skip_blanket_find=True)._onchange_quantity() + + @api.constrains("date_planned") + def check_date_planned(self): + for line in self: + date_planned = fields.Date.from_string(line.date_planned) + if ( + line.blanket_order_line + and line.blanket_order_line.date_schedule + and line.blanket_order_line.date_schedule != date_planned + ): + raise ValidationError( + _( + "Schedule dates defined on the Purchase Order Line " + "and on the Blanket Order Line do not match." + ) + ) + + @api.constrains("currency_id") + def check_currency(self): + for line in self: + blanket_currency = line.blanket_order_line.order_id.currency_id + if blanket_currency and line.order_id.currency_id != blanket_currency: + raise ValidationError( + _( + "The currency of the blanket order must match with that " + "of the purchase order." + ) + ) diff --git a/purchase_blanket_order/readme/CONTRIBUTORS.rst b/purchase_blanket_order/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..d7aa0e46b7d --- /dev/null +++ b/purchase_blanket_order/readme/CONTRIBUTORS.rst @@ -0,0 +1,5 @@ +* Adrià Gil Sorribes +* Jordi Ballester Alomar +* Héctor Villarreal +* Lois Rilo +* Dhara Solanki diff --git a/purchase_blanket_order/readme/DESCRIPTION.rst b/purchase_blanket_order/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..e4d52f2b90d --- /dev/null +++ b/purchase_blanket_order/readme/DESCRIPTION.rst @@ -0,0 +1,4 @@ +A purchase blanket order is a pre-agreement to purchase a certain number of +quantities of products at a specific price. From a confirmed blanket order, +the users can create new purchase orders at such price, until the blanket +order expires due to reaching the validity date. diff --git a/purchase_blanket_order/readme/USAGE.rst b/purchase_blanket_order/readme/USAGE.rst new file mode 100644 index 00000000000..fe1391dbcb6 --- /dev/null +++ b/purchase_blanket_order/readme/USAGE.rst @@ -0,0 +1,52 @@ +A new menu in the Purchase area is created, allowing users to create new blanket orders. + +To create a new Purchase Blanket Order go to the purchase menu in the Purchase section: + +.. image:: /purchase_blanket_order/static/description/BO_menu.png + :alt: Blanket Orders menu + +Hitting the button create will open the form view in which we can introduce the following +information: + +* Vendor +* Payment Terms +* Ordering and Validity dates +* Order lines: + * Product + * Accorded price + * Original, Ordered, Invoiced, Received and Remaining quantities +* Terms and Conditions of the Blanket Order + +.. image:: /purchase_blanket_order/static/description/BO_form.png + :alt: Blanket Orders form + +From the form, once the Blanket Order has been confirmed and its state is open, the user can +create a Purchase Order, check the Purchase Orders associated to the Blanket Order and/or +see the Blanket Order lines associated to the BO. + +.. image:: /purchase_blanket_order/static/description/BO_actions.png + :alt: Actions that can be done from Blanket Order + +Hitting the button Create Purchase Order will open a wizard that will ask for the amount of each +product in the BO lines for which the Purchase Order will be created. + +.. image:: /purchase_blanket_order/static/description/PO_from_BO.png + :alt: Create Purchase Order from Blanket Order + +Installing this module will add an additional menu which will show all the blanket order lines +currently defined in the system. From this list the user can create customized Purchase Orders +selecting the lines for which the PO (or POs if the vendors are different) is (are) created. + +.. image:: /purchase_blanket_order/static/description/BO_lines.png + :alt: Blanket Order lines and actions + +In the Purchase Order form one field is added in the PO lines, the Blanket Order line field. This +field keeps track to which Blanket Order line the PO line is associated. Upon adding a new product +in a newly created Purchase Order a blanket order line will be suggested depending on the following +factors: + +* Closer Validity date +* Remaining quantity > Quantity introduced in the Purchase Order line + +.. image:: /purchase_blanket_order/static/description/PO_BOLine.png + :alt: New field added in Purchase Order Line diff --git a/purchase_blanket_order/report/report.xml b/purchase_blanket_order/report/report.xml new file mode 100644 index 00000000000..fab3d23f2eb --- /dev/null +++ b/purchase_blanket_order/report/report.xml @@ -0,0 +1,12 @@ + + + + Purchase Blanket Order + purchase.blanket.order + qweb-pdf + purchase_blanket_order.report_blanketorder + purchase_blanket_order.report_blanketorder + + report + + diff --git a/purchase_blanket_order/report/templates.xml b/purchase_blanket_order/report/templates.xml new file mode 100644 index 00000000000..d1d765f5364 --- /dev/null +++ b/purchase_blanket_order/report/templates.xml @@ -0,0 +1,132 @@ + + + + + diff --git a/purchase_blanket_order/security/ir.model.access.csv b/purchase_blanket_order/security/ir.model.access.csv new file mode 100644 index 00000000000..5b6bf71a4e3 --- /dev/null +++ b/purchase_blanket_order/security/ir.model.access.csv @@ -0,0 +1,11 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_purchase_blanket_order,purchase.order,model_purchase_blanket_order,purchase.group_purchase_manager,1,1,1,0 +access_purchase_blanket_order_line,purchase.order.line,model_purchase_blanket_order_line,purchase.group_purchase_manager,1,1,1,1 +access_purchase_blanket_order_manager,purchase.order.manager,model_purchase_blanket_order,purchase.group_purchase_manager,1,1,1,1 +access_purchase_blanket_order_line_manager,purchase.order.line.manager,model_purchase_blanket_order_line,purchase.group_purchase_manager,1,1,1,1 +access_purchase_blanket_order_accountant,purchase.order.accountant,model_purchase_blanket_order,account.group_account_user,1,1,0,0 +access_purchase_blanket_order_line_accountant,purchase.order.line accountant,model_purchase_blanket_order_line,account.group_account_user,1,1,0,0 +access_purchase_blanket_order_user,purchase.order user,model_purchase_blanket_order,base.group_user,1,0,0,0 +access_purchase_blanket_order_line_user,purchase.order.line user,model_purchase_blanket_order_line,base.group_user,1,0,0,0 +access_purchase_blanket_order_wizard_user,purchase.blanket.order.wizard user,model_purchase_blanket_order_wizard,base.group_user,1,1,1,0 +access_purchase_blanket_order_wizard_line_user,purchase.blanket.order.wizard.line user,model_purchase_blanket_order_wizard_line,base.group_user,1,1,1,1 diff --git a/purchase_blanket_order/security/security.xml b/purchase_blanket_order/security/security.xml new file mode 100644 index 00000000000..520e6d6f2be --- /dev/null +++ b/purchase_blanket_order/security/security.xml @@ -0,0 +1,25 @@ + + + + Disable adding more lines to POs from Purchase Blanket Orders + + + + Blanket Order multi-company + + + ['|', ('company_id','=',False), ('company_id','in',company_ids)] + + + Blanket Order Line multi-company + + + ['|', ('company_id','=',False), ('company_id','in',company_ids)] + + diff --git a/purchase_blanket_order/static/description/BO_actions.png b/purchase_blanket_order/static/description/BO_actions.png new file mode 100644 index 00000000000..e05cce81957 Binary files /dev/null and b/purchase_blanket_order/static/description/BO_actions.png differ diff --git a/purchase_blanket_order/static/description/BO_form.png b/purchase_blanket_order/static/description/BO_form.png new file mode 100644 index 00000000000..f3ecb5ea636 Binary files /dev/null and b/purchase_blanket_order/static/description/BO_form.png differ diff --git a/purchase_blanket_order/static/description/BO_lines.png b/purchase_blanket_order/static/description/BO_lines.png new file mode 100644 index 00000000000..4724cf97d48 Binary files /dev/null and b/purchase_blanket_order/static/description/BO_lines.png differ diff --git a/purchase_blanket_order/static/description/BO_menu.png b/purchase_blanket_order/static/description/BO_menu.png new file mode 100644 index 00000000000..0633ba833b3 Binary files /dev/null and b/purchase_blanket_order/static/description/BO_menu.png differ diff --git a/purchase_blanket_order/static/description/PO_BOLine.png b/purchase_blanket_order/static/description/PO_BOLine.png new file mode 100644 index 00000000000..630baea88b4 Binary files /dev/null and b/purchase_blanket_order/static/description/PO_BOLine.png differ diff --git a/purchase_blanket_order/static/description/PO_from_BO.png b/purchase_blanket_order/static/description/PO_from_BO.png new file mode 100644 index 00000000000..bdedee36eb7 Binary files /dev/null and b/purchase_blanket_order/static/description/PO_from_BO.png differ diff --git a/purchase_blanket_order/static/description/icon.png b/purchase_blanket_order/static/description/icon.png new file mode 100644 index 00000000000..3a0328b516c Binary files /dev/null and b/purchase_blanket_order/static/description/icon.png differ diff --git a/purchase_blanket_order/static/description/index.html b/purchase_blanket_order/static/description/index.html new file mode 100644 index 00000000000..268ab21c573 --- /dev/null +++ b/purchase_blanket_order/static/description/index.html @@ -0,0 +1,472 @@ + + + + + + +Purchase Blanket Orders + + + +
+

Purchase Blanket Orders

+ + +

Beta License: AGPL-3 OCA/purchase-workflow Translate me on Weblate Try me on Runbot

+

A purchase blanket order is a pre-agreement to purchase a certain number of +quantities of products at a specific price. From a confirmed blanket order, +the users can create new purchase orders at such price, until the blanket +order expires due to reaching the validity date.

+

Table of contents

+ +
+

Usage

+

A new menu in the Purchase area is created, allowing users to create new blanket orders.

+

To create a new Purchase Blanket Order go to the purchase menu in the Purchase section:

+Blanket Orders menu +

Hitting the button create will open the form view in which we can introduce the following +information:

+
    +
  • Vendor
  • +
  • Payment Terms
  • +
  • Ordering and Validity dates
  • +
  • +
    Order lines:
    +
      +
    • Product
    • +
    • Accorded price
    • +
    • Original, Ordered, Invoiced, Received and Remaining quantities
    • +
    +
    +
    +
  • +
  • Terms and Conditions of the Blanket Order
  • +
+Blanket Orders form +

From the form, once the Blanket Order has been confirmed and its state is open, the user can +create a Purchase Order, check the Purchase Orders associated to the Blanket Order and/or +see the Blanket Order lines associated to the BO.

+Actions that can be done from Blanket Order +

Hitting the button Create Purchase Order will open a wizard that will ask for the amount of each +product in the BO lines for which the Purchase Order will be created.

+Create Purchase Order from Blanket Order +

Installing this module will add an additional menu which will show all the blanket order lines +currently defined in the system. From this list the user can create customized Purchase Orders +selecting the lines for which the PO (or POs if the vendors are different) is (are) created.

+Blanket Order lines and actions +

In the Purchase Order form one field is added in the PO lines, the Blanket Order line field. This +field keeps track to which Blanket Order line the PO line is associated. Upon adding a new product +in a newly created Purchase Order a blanket order line will be suggested depending on the following +factors:

+
    +
  • Closer Validity date
  • +
  • Remaining quantity > Quantity introduced in the Purchase Order line
  • +
+New field added in Purchase Order Line +
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • ForgeFlow
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

This module is part of the OCA/purchase-workflow project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/purchase_blanket_order/tests/__init__.py b/purchase_blanket_order/tests/__init__.py new file mode 100644 index 00000000000..21834e24230 --- /dev/null +++ b/purchase_blanket_order/tests/__init__.py @@ -0,0 +1,4 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import test_purchase_blanket_order +from . import test_purchase_order diff --git a/purchase_blanket_order/tests/test_purchase_blanket_order.py b/purchase_blanket_order/tests/test_purchase_blanket_order.py new file mode 100644 index 00000000000..5ec432165c6 --- /dev/null +++ b/purchase_blanket_order/tests/test_purchase_blanket_order.py @@ -0,0 +1,228 @@ +# Copyright (C) 2018 ForgeFlow S.L. (https://www.forgeflow.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from datetime import date, timedelta + +from odoo import fields +from odoo.exceptions import UserError +from odoo.tests import common + + +class TestPurchaseBlanketOrders(common.TransactionCase): + def setUp(self): + super().setUp() + self.blanket_order_obj = self.env["purchase.blanket.order"] + self.blanket_order_line_obj = self.env["purchase.blanket.order.line"] + self.blanket_order_wiz_obj = self.env["purchase.blanket.order.wizard"] + + self.partner = self.env["res.partner"].create( + {"name": "TEST SUPPLIER", "supplier_rank": 1} + ) + self.payment_term = self.env.ref("account.account_payment_term_30days") + + # Seller IDS + seller = self.env["product.supplierinfo"].create( + {"partner_id": self.partner.id, "price": 30.0} + ) + + self.product = self.env["product.product"].create( + { + "name": "Demo", + "categ_id": self.env.ref("product.product_category_1").id, + "standard_price": 35.0, + "seller_ids": [(6, 0, [seller.id])], + "type": "consu", + "uom_id": self.env.ref("uom.product_uom_unit").id, + "default_code": "PROD_DEL01", + } + ) + self.product2 = self.env["product.product"].create( + { + "name": "Demo 2", + "categ_id": self.env.ref("product.product_category_1").id, + "standard_price": 50.0, + "type": "consu", + "uom_id": self.env.ref("uom.product_uom_unit").id, + "default_code": "PROD_DEL02", + } + ) + + self.yesterday = date.today() - timedelta(days=1) + self.tomorrow = date.today() + timedelta(days=1) + + def _get_po_from_wizard(self, res): + return self.env[res["res_model"]].search(res["domain"]) + + def test_01_create_blanket_order_flow(self): + """We create a blanket order and check constrains to confirm BO""" + blanket_order = self.blanket_order_obj.create( + { + "partner_id": self.partner.id, + "validity_date": fields.Date.to_string(self.yesterday), + "payment_term_id": self.payment_term.id, + "line_ids": [ + ( + 0, + 0, + { + "product_id": self.product.id, + "product_uom": self.product.uom_id.id, + "original_uom_qty": 20.0, + "price_unit": 0.0, # will be updated later + }, + ) + ], + } + ) + blanket_order.sudo().onchange_partner_id() + blanket_order.line_ids[0].sudo().onchange_product() + blanket_order._compute_line_count() + blanket_order._compute_uom_qty() + + self.assertEqual(blanket_order.state, "draft") + self.assertEqual(blanket_order.line_ids[0].price_unit, 30.0) + self.assertEqual(blanket_order.original_uom_qty, 20.0) + self.assertEqual(blanket_order.ordered_uom_qty, 0.0) + self.assertEqual(blanket_order.remaining_uom_qty, 20.0) + + # date in the past + with self.assertRaises(UserError): + blanket_order.sudo().action_confirm() + + blanket_order.validity_date = fields.Date.to_string(self.tomorrow) + initial_name = blanket_order.name + blanket_order.sudo().action_confirm() + self.assertNotEqual(initial_name, blanket_order.name) + + blanket_order.sudo().action_cancel() + self.assertEqual(blanket_order.state, "expired") + blanket_order.sudo().set_to_draft() + self.assertEqual(blanket_order.state, "draft") + previous_name = blanket_order.name + blanket_order.sudo().action_confirm() + self.assertEqual(previous_name, blanket_order.name) + + self.assertEqual(blanket_order.state, "open") + blanket_order.action_view_purchase_blanket_order_line() + + # Search view check + blanket_order._search_original_uom_qty(">=", 0.0) + blanket_order._search_ordered_uom_qty(">=", 0.0) + blanket_order._search_invoiced_uom_qty(">=", 0.0) + blanket_order._search_received_uom_qty(">=", 0.0) + blanket_order._search_remaining_uom_qty(">=", 0.0) + + def test__02_create_purchase_orders_from_blanket_order(self): + """We create a blanket order and create two purchase orders""" + blanket_order = self.blanket_order_obj.create( + { + "partner_id": self.partner.id, + "partner_ref": "REF", + "validity_date": fields.Date.to_string(self.tomorrow), + "payment_term_id": self.payment_term.id, + "line_ids": [ + ( + 0, + 0, + { + "product_id": self.product.id, + "product_uom": self.product.uom_id.id, + "original_uom_qty": 20.0, + "price_unit": 30.0, + }, + ) + ], + } + ) + blanket_order.sudo().onchange_partner_id() + + with self.assertRaises(UserError): + # Blanket order is not confirmed + self.blanket_order_wiz_obj.with_context( + active_id=blanket_order.id, active_model="purchase.blanket.order" + ).create({}) + blanket_order.sudo().action_confirm() + + wizard1 = self.blanket_order_wiz_obj.with_context( + active_id=blanket_order.id, active_model="purchase.blanket.order" + ).create({}) + wizard1.line_ids[0].write({"qty": 30.0}) + + with self.assertRaises(UserError): + # Wizard quantity greater than remaining quantity + wizard1.sudo().create_purchase_order() + wizard1.line_ids[0].write({"qty": 10.0}) + res = wizard1.sudo().create_purchase_order() + po = self._get_po_from_wizard(res) + self.assertEqual(po.partner_ref, "REF") + + wizard2 = self.blanket_order_wiz_obj.with_context( + active_id=blanket_order.id, active_model="purchase.blanket.order" + ).create({}) + wizard2.line_ids[0].write({"qty": 10.0}) + res = wizard2.sudo().create_purchase_order() + po = self._get_po_from_wizard(res) + self.assertEqual(po.partner_ref, "REF") + + with self.assertRaises(UserError): + # Blanket order already completed + self.blanket_order_wiz_obj.with_context( + active_id=blanket_order.id, active_model="purchase.blanket.order" + ).create({}) + + self.assertEqual(blanket_order.state, "done") + + self.assertEqual(blanket_order.purchase_count, 2) + + view_action = blanket_order.action_view_purchase_orders() + domain_ids = view_action["domain"][0][2] + self.assertEqual(len(domain_ids), 2) + + def test_03_create_purchase_orders_from_blanket_order_line(self): + """We create a blanket order and create two purchase orders + from the blanket order lines""" + blanket_order = self.blanket_order_obj.create( + { + "partner_id": self.partner.id, + "partner_ref": "REF", + "validity_date": fields.Date.to_string(self.tomorrow), + "payment_term_id": self.payment_term.id, + "line_ids": [ + ( + 0, + 0, + { + "product_id": self.product.id, + "product_uom": self.product.uom_id.id, + "original_uom_qty": 20.0, + "price_unit": 30.0, + }, + ), + ( + 0, + 0, + { + "product_id": self.product2.id, + "product_uom": self.product2.uom_id.id, + "original_uom_qty": 50.0, + "price_unit": 60.0, + }, + ), + ], + } + ) + blanket_order.sudo().onchange_partner_id() + blanket_order.sudo().action_confirm() + + bo_lines = blanket_order.line_ids + self.assertEqual(blanket_order.line_count, 2) + + wizard1 = self.blanket_order_wiz_obj.with_context( + active_ids=[bo_lines[0].id, bo_lines[1].id] + ).create({}) + self.assertEqual(len(wizard1.line_ids), 2) + wizard1.line_ids[0].write({"qty": 10.0}) + wizard1.line_ids[1].write({"qty": 20.0}) + wizard1.sudo().create_purchase_order() + + self.assertEqual(bo_lines[0].remaining_uom_qty, 10.0) + self.assertEqual(bo_lines[1].remaining_uom_qty, 30.0) diff --git a/purchase_blanket_order/tests/test_purchase_order.py b/purchase_blanket_order/tests/test_purchase_order.py new file mode 100644 index 00000000000..50f42ff08c9 --- /dev/null +++ b/purchase_blanket_order/tests/test_purchase_order.py @@ -0,0 +1,202 @@ +# Copyright (C) 2018 ForgeFlow S.L. (https://www.forgeflow.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from datetime import date, timedelta + +from odoo import fields +from odoo.tests import common + + +class TestPurchaseOrder(common.TransactionCase): + def setUp(self): + super().setUp() + self.blanket_order_obj = self.env["purchase.blanket.order"] + self.blanket_order_line_obj = self.env["purchase.blanket.order.line"] + self.purchase_order_obj = self.env["purchase.order"] + self.purchase_order_line_obj = self.env["purchase.order.line"] + + self.partner = self.env["res.partner"].create( + {"name": "TEST SUPPLIER", "supplier_rank": 1} + ) + self.payment_term = self.env.ref("account.account_payment_term_30days") + + # Seller IDS + seller = self.env["product.supplierinfo"].create( + {"partner_id": self.partner.id, "price": 30.0} + ) + + self.product = self.env["product.product"].create( + { + "name": "Demo", + "categ_id": self.env.ref("product.product_category_1").id, + "standard_price": 35.0, + "seller_ids": [(6, 0, [seller.id])], + "type": "consu", + "uom_id": self.env.ref("uom.product_uom_unit").id, + "default_code": "PROD_DEL01", + } + ) + self.product_2 = self.env["product.product"].create( + { + "name": "Demo 2", + "categ_id": self.env.ref("product.product_category_1").id, + "standard_price": 35.0, + "seller_ids": [(6, 0, [seller.id])], + "type": "consu", + "uom_id": self.env.ref("uom.product_uom_unit").id, + "default_code": "PROD_DEL02", + } + ) + self.validity = date.today() + timedelta(days=365) + self.date_schedule_1 = date.today() + timedelta(days=10) + self.date_schedule_2 = date.today() + timedelta(days=20) + + def create_blanket_order_01(self): + blanket_order = self.blanket_order_obj.create( + { + "partner_id": self.partner.id, + "validity_date": fields.Date.to_string(self.validity), + "payment_term_id": self.payment_term.id, + "line_ids": [ + ( + 0, + 0, + { + "product_id": self.product.id, + "product_uom": self.product.uom_id.id, + "date_schedule": fields.Date.to_string( + self.date_schedule_1 + ), + "original_uom_qty": 20.0, + "price_unit": 30.0, + }, + ), + ( + 0, + 0, + { + "product_id": self.product.id, + "product_uom": self.product.uom_id.id, + "date_schedule": fields.Date.to_string( + self.date_schedule_2 + ), + "original_uom_qty": 20.0, + "price_unit": 30.0, + }, + ), + ], + } + ) + blanket_order.sudo().onchange_partner_id() + return blanket_order + + def create_blanket_order_02(self): + blanket_order = self.blanket_order_obj.create( + { + "partner_id": self.partner.id, + "validity_date": fields.Date.to_string(self.validity), + "payment_term_id": self.payment_term.id, + "line_ids": [ + ( + 0, + 0, + { + "product_id": self.product.id, + "product_uom": self.product.uom_id.id, + "original_uom_qty": 20.0, + "price_unit": 30.0, + }, + ), + ( + 0, + 0, + { + "product_id": self.product_2.id, + "product_uom": self.product.uom_id.id, + "original_uom_qty": 20.0, + "price_unit": 30.0, + }, + ), + ], + } + ) + blanket_order.sudo().onchange_partner_id() + return blanket_order + + def test_01_create_purchase_order(self): + blanket_order = self.create_blanket_order_01() + blanket_order.sudo().action_confirm() + bo_lines = self.blanket_order_line_obj.search( + [("order_id", "=", blanket_order.id)] + ) + self.assertEqual(len(bo_lines), 2) + + po = self.purchase_order_obj.create( + { + "partner_id": self.partner.id, + "order_line": [ + ( + 0, + 0, + { + "name": self.product.name, + "product_id": self.product.id, + "product_qty": 5.0, + "product_uom": self.product.uom_po_id.id, + "date_planned": date.today(), + "price_unit": 10.0, + }, + ) + ], + } + ) + po_line = po.order_line[0] + po_line.with_context(from_purchase_order=True).name_get() + po_line.onchange_product_id() + self.assertEqual(po_line._get_eligible_bo_lines(), bo_lines) + bo_line_assigned = self.blanket_order_line_obj.search( + [("date_schedule", "=", fields.Date.to_string(self.date_schedule_1))] + ) + self.assertEqual(po_line.blanket_order_line, bo_line_assigned) + + def test_02_create_purchase_order(self): + blanket_order = self.create_blanket_order_02() + blanket_order.sudo().action_confirm() + bo_lines = self.blanket_order_line_obj.search( + [("order_id", "=", blanket_order.id)] + ) + self.assertEqual(len(bo_lines), 2) + + po = self.purchase_order_obj.create( + { + "partner_id": self.partner.id, + "order_line": [ + ( + 0, + 0, + { + "name": self.product.name, + "product_id": self.product.id, + "product_qty": 5.0, + "product_uom": self.product.uom_po_id.id, + "date_planned": date.today(), + "price_unit": 10.0, + }, + ) + ], + } + ) + po_line = po.order_line[0] + po_line.with_context(from_purchase_order=True).name_get() + po_line.onchange_product_id() + self.assertEqual( + po_line._get_eligible_bo_lines(), + bo_lines.filtered(lambda l: l.product_id == self.product), + ) + bo_line_assigned = self.blanket_order_line_obj.search( + [ + ("order_id", "=", blanket_order.id), + ("product_id", "=", self.product.id), + ("date_schedule", "=", False), + ] + ) + self.assertEqual(po_line.blanket_order_line, bo_line_assigned) diff --git a/purchase_blanket_order/views/purchase_blanket_order_views.xml b/purchase_blanket_order/views/purchase_blanket_order_views.xml new file mode 100644 index 00000000000..987a04743ac --- /dev/null +++ b/purchase_blanket_order/views/purchase_blanket_order_views.xml @@ -0,0 +1,439 @@ + + + + + purchase.blanket.order.tree + purchase.blanket.order + + + + + + + + + + + + + + purchase.blanket.order.form + purchase.blanket.order + +
+
+
+ +
+ + +
+
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + +
+ + + + + purchase.blanket.order.search + purchase.blanket.order + + + + + + + + + + + + + + + + + + + Blanket Orders + ir.actions.act_window + purchase.blanket.order + tree,form + + [] + {} + +

+ Click to create a blanket order that can be converted into a purchase order. +

+

+ Use this menu to search within your blanket orders. For each blanket order, + you can track the related discussion with the vendor, control + the products received and control the vendor bills. +

+
+
+ + purchase.blanket.order.line.tree + purchase.blanket.order.line + + + + + + + + + + + + + + + + + + + purchase.blanket.order.line.form + purchase.blanket.order.line + + +
+ +

+ +

+ + + + + + + + + + + + + + + + + +
+
+ + + +
+
+
+
+ + purchase.blanket.order.line.search + purchase.blanket.order.line + + + + + + + + + Blanket Order Lines + ir.actions.act_window + purchase.blanket.order.line + tree,form + + + + + diff --git a/purchase_blanket_order/views/purchase_config_settings.xml b/purchase_blanket_order/views/purchase_config_settings.xml new file mode 100644 index 00000000000..ae30a49aaa3 --- /dev/null +++ b/purchase_blanket_order/views/purchase_config_settings.xml @@ -0,0 +1,34 @@ + + + + + res.config.settings.form (in purchase_blanket_order) + res.config.settings + + + +

Blanket Orders

+
+
+
+ +
+
+
+
+
+
+
+
+
diff --git a/purchase_blanket_order/views/purchase_order_views.xml b/purchase_blanket_order/views/purchase_order_views.xml new file mode 100644 index 00000000000..ef22a1b3c54 --- /dev/null +++ b/purchase_blanket_order/views/purchase_order_views.xml @@ -0,0 +1,40 @@ + + + + + purchase.order.from.blanket.form + purchase.order + + + + + + + {'from_purchase_order': True} + + + + + purchase.order.from.blanket.form - disable adding lines + purchase.order + + + + blanket_order_id==False + + + + diff --git a/purchase_blanket_order/wizard/__init__.py b/purchase_blanket_order/wizard/__init__.py new file mode 100644 index 00000000000..c45a6c80631 --- /dev/null +++ b/purchase_blanket_order/wizard/__init__.py @@ -0,0 +1,3 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import create_purchase_orders diff --git a/purchase_blanket_order/wizard/create_purchase_orders.py b/purchase_blanket_order/wizard/create_purchase_orders.py new file mode 100644 index 00000000000..ece0f18a1b5 --- /dev/null +++ b/purchase_blanket_order/wizard/create_purchase_orders.py @@ -0,0 +1,221 @@ +# Copyright (C) 2018 ForgeFlow S.L. (https://www.forgeflow.com) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from collections import defaultdict + +from odoo import _, api, fields, models +from odoo.exceptions import UserError +from odoo.tools import float_is_zero + + +class BlanketOrderWizard(models.TransientModel): + _name = "purchase.blanket.order.wizard" + _description = "Blanket Order Wizard" + + @api.model + def _default_order(self): + # in case the cron hasn't run + active_model = self.env.context.get("active_model", False) + self.env["purchase.blanket.order"].expire_orders() + if ( + not self.env.context.get("active_id") + or active_model != "purchase.blanket.order" + ): + return False + blanket_order = self.env["purchase.blanket.order"].search( + [("id", "=", self.env.context["active_id"])], limit=1 + ) + if blanket_order.state == "expired": + raise UserError( + _("You can't create a purchase order from an expired blanket order!") + ) + return blanket_order + + @api.model + def _check_valid_blanket_order_line(self, bo_lines): + precision = self.env["decimal.precision"].precision_get( + "Product Unit of Measure" + ) + company_id = False + + if float_is_zero( + sum(bo_lines.mapped("remaining_uom_qty")), precision_digits=precision + ): + raise UserError(_("All lines have already been completed.")) + + for line in bo_lines: + + if line.order_id.state != "open": + raise UserError( + _("Purchase Blanket Order %s is not open") % line.order_id.name + ) + + line_company_id = line.company_id and line.company_id.id or False + if company_id is not False and line_company_id != company_id: + raise UserError(_("You have to select lines from the same company.")) + else: + company_id = line_company_id + + @api.model + def _default_lines(self): + blanket_order_line_obj = self.env["purchase.blanket.order.line"] + blanket_order_line_ids = self.env.context.get("active_ids", False) + active_model = self.env.context.get("active_model", False) + + if active_model == "purchase.blanket.order": + bo_lines = self._default_order().line_ids + else: + bo_lines = blanket_order_line_obj.browse(blanket_order_line_ids) + + self._check_valid_blanket_order_line(bo_lines) + + lines = [ + ( + 0, + 0, + { + "blanket_line_id": line.id, + "product_id": line.product_id.id, + "date_schedule": line.date_schedule, + "remaining_uom_qty": line.remaining_uom_qty, + "price_unit": line.price_unit, + "product_uom": line.product_uom, + "qty": line.remaining_uom_qty, + "partner_id": line.partner_id, + }, + ) + for line in bo_lines + if line.remaining_uom_qty > 0 + ] + return lines + + blanket_order_id = fields.Many2one( + "purchase.blanket.order", readonly=True, default=_default_order + ) + purchase_order_id = fields.Many2one( + "purchase.order", + string="Purchase Order", + domain=[("state", "=", "draft")], + ) + line_ids = fields.One2many( + "purchase.blanket.order.wizard.line", + "wizard_id", + string="Lines", + default=_default_lines, + ) + + def create_purchase_order(self): + + order_lines_by_supplier = defaultdict(list) + currency_id = 0 + payment_term_id = 0 + for line in self.line_ids: + if line.qty == 0.0: + continue + + if line.qty > line.remaining_uom_qty: + raise UserError(_("You can't order more than the remaining quantities")) + + date_planned = line.blanket_line_id.date_schedule + + vals = { + "product_id": line.product_id.id, + "name": line.product_id.name, + "date_planned": date_planned + if date_planned + else line.blanket_line_id.order_id.date_start, + "product_uom": line.product_uom.id, + "sequence": line.blanket_line_id.sequence, + "price_unit": line.blanket_line_id.price_unit, + "blanket_order_line": line.blanket_line_id.id, + "product_qty": line.qty, + "taxes_id": [(6, 0, line.taxes_id.ids)], + } + order_lines_by_supplier[line.partner_id.id].append((0, 0, vals)) + + if currency_id == 0: + currency_id = line.blanket_line_id.order_id.currency_id.id + elif currency_id != line.blanket_line_id.order_id.currency_id.id: + currency_id = False + + if payment_term_id == 0: + payment_term_id = line.blanket_line_id.payment_term_id.id + elif payment_term_id != line.blanket_line_id.payment_term_id.id: + payment_term_id = False + + if not order_lines_by_supplier: + raise UserError(_("An order can't be empty")) + + if not currency_id: + raise UserError( + _( + "Can not create Purchase Order from Blanket " + "Order lines with different currencies" + ) + ) + + res = [] + for supplier in order_lines_by_supplier: + order_vals = { + "partner_id": int(supplier), + } + if self.blanket_order_id: + order_vals.update( + { + "partner_ref": self.blanket_order_id.partner_ref, + "origin": self.blanket_order_id.name, + } + ) + order_vals.update( + { + "currency_id": currency_id if currency_id else False, + "payment_term_id": (payment_term_id if payment_term_id else False), + "order_line": order_lines_by_supplier[supplier], + } + ) + purchase_order = self.env["purchase.order"].create(order_vals) + res.append(purchase_order.id) + return { + "domain": [("id", "in", res)], + "name": _("RFQ"), + "view_mode": "tree,form", + "res_model": "purchase.order", + "view_id": False, + "context": {"from_purchase_order": True}, + "type": "ir.actions.act_window", + } + + +class BlanketOrderWizardLine(models.TransientModel): + _name = "purchase.blanket.order.wizard.line" + _description = "Purchase Blanket Order Wizard Line" + + wizard_id = fields.Many2one("purchase.blanket.order.wizard") + blanket_line_id = fields.Many2one("purchase.blanket.order.line") + product_id = fields.Many2one( + "product.product", + related="blanket_line_id.product_id", + string="Product", + readonly=True, + ) + product_uom = fields.Many2one( + "uom.uom", + related="blanket_line_id.product_uom", + string="Unit of Measure", + readonly=True, + ) + date_schedule = fields.Date(related="blanket_line_id.date_schedule", readonly=True) + remaining_uom_qty = fields.Float( + related="blanket_line_id.remaining_uom_qty", readonly=True + ) + qty = fields.Float(string="Quantity to Order", required=True) + price_unit = fields.Float(related="blanket_line_id.price_unit", readonly=True) + currency_id = fields.Many2one("res.currency", related="blanket_line_id.currency_id") + partner_id = fields.Many2one( + "res.partner", + related="blanket_line_id.partner_id", + string="Vendor", + readonly=True, + ) + taxes_id = fields.Many2many( + "account.tax", related="blanket_line_id.taxes_id", readonly=True + ) diff --git a/purchase_blanket_order/wizard/create_purchase_orders.xml b/purchase_blanket_order/wizard/create_purchase_orders.xml new file mode 100644 index 00000000000..7750a32d78f --- /dev/null +++ b/purchase_blanket_order/wizard/create_purchase_orders.xml @@ -0,0 +1,45 @@ + + + + Create Purchase Order + purchase.blanket.order.wizard + +
+
+ + + + + + + + + + + + +
+
+
+
+
+
+ + Create RFQ + ir.actions.act_window + purchase.blanket.order.wizard + form + new + + +
diff --git a/setup/purchase_blanket_order/odoo/addons/purchase_blanket_order b/setup/purchase_blanket_order/odoo/addons/purchase_blanket_order new file mode 120000 index 00000000000..9bbca1bfe3e --- /dev/null +++ b/setup/purchase_blanket_order/odoo/addons/purchase_blanket_order @@ -0,0 +1 @@ +../../../../purchase_blanket_order \ No newline at end of file diff --git a/setup/purchase_blanket_order/setup.py b/setup/purchase_blanket_order/setup.py new file mode 100644 index 00000000000..28c57bb6403 --- /dev/null +++ b/setup/purchase_blanket_order/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)