From 3546b1e84619689ce7f48fd389d5f5b8261d7be9 Mon Sep 17 00:00:00 2001 From: Alexis de Lattre Date: Mon, 25 May 2015 02:43:43 +0200 Subject: [PATCH] Port to v8 and new API with several enhancements --- sale_rental/README.rst | 79 ++ sale_rental/__init__.py | 4 +- sale_rental/__openerp__.py | 34 +- sale_rental/i18n/sale_rental.pot | 327 +++--- sale_rental/product_view.xml | 3 +- sale_rental/rental.py | 1035 +++++++++-------- sale_rental/rental_data.xml | 32 +- sale_rental/rental_demo.xml | 79 +- sale_rental/rental_view.xml | 19 +- sale_rental/sale_view.xml | 30 +- sale_rental/stock_view.xml | 15 +- sale_rental/wizard/__init__.py | 4 +- sale_rental/wizard/create_rental_product.py | 113 +- .../wizard/create_rental_product_view.xml | 5 +- sale_start_end_dates/README.rst | 29 + sale_start_end_dates/__init__.py | 4 +- sale_start_end_dates/__openerp__.py | 22 +- .../i18n/sale_start_end_dates.pot | 60 +- sale_start_end_dates/sale.py | 195 ++-- sale_start_end_dates/sale_demo.xml | 24 + sale_start_end_dates/sale_view.xml | 30 +- 21 files changed, 1258 insertions(+), 885 deletions(-) create mode 100644 sale_rental/README.rst create mode 100644 sale_start_end_dates/README.rst create mode 100644 sale_start_end_dates/sale_demo.xml diff --git a/sale_rental/README.rst b/sale_rental/README.rst new file mode 100644 index 00000000000..4715e9a006c --- /dev/null +++ b/sale_rental/README.rst @@ -0,0 +1,79 @@ +Sale Rental +=========== + +With this module, you can rent products with Odoo. This module supports: + +* regular rentals, +* rental extensions, +* sale of rented products. + +Configuration +============= + +In the menu *Sales > Products > Product Variants*, on the form view +of a stockable product or consumable, in the *Rental* tab, there is a +button *Create Rental Service* which starts a wizard to generate the +corresponding rental service. + +In the menu *Warehouse > Configuration > Warehouses*, on the form view +of the warehouse, in the *Technical Information* tab, you will see two +additionnal stock locations: *Rental In* (stock of products to rent) and +*Rental Out* (products currently rented). In the *Warehouse Configuration* tab, +make sure that the option *Rental Allowed* is checked. + +To use the module, you need to have access to the form view of sale +order lines. For that, you must add your user to one of these groups: + +* Manage Product Packaging +* Properties on lines + +Usage +===== + +In a sale order line (form view, not tree view), if you select a rental +service, you can : + +* create a new rental with a start date and an end date: when the sale + order is confirmed, it will generate a delivery order and an incoming + shipment. +* extend an existing rental: the incoming shipment will be postponed to + the end date of the extension. + +In a sale order line, if you select a product that has a corresponding +rental service, you can decide to sell the rented product that the +customer already has. If the sale order is confirmed, the incoming +shipment will be cancelled and a new delivery order will be created with +a stock move from *Rental Out* to *Customers*. + +Please refer to `this screencast ` +to get a demo of the installation, configuration and use of this module +(note that this screencast is for Odoo v7, not v8). + +Known issues / Roadmap +====================== + +This module has the following limitations: + + * No support for planning/agenda of the rented products + * the unit of measure of the rental services must be *Day* (the rental per hours / per week / per month is not supported for the moment) + +Credits +======= + +Contributors +------------ + +* Alexis de Lattre + +Maintainer +---------- + +.. image:: http://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: http://odoo-community.org + +This module is maintained by the OCA. + +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. + +To contribute to this module, please visit http://odoo-community.org. diff --git a/sale_rental/__init__.py b/sale_rental/__init__.py index 09b1d640f0e..731e3a0e510 100644 --- a/sale_rental/__init__.py +++ b/sale_rental/__init__.py @@ -1,8 +1,8 @@ # -*- encoding: utf-8 -*- ############################################################################## # -# Sale Rental module for OpenERP -# Copyright (C) 2014 Akretion (http://www.akretion.com) +# Sale Rental module for Odoo +# Copyright (C) 2014-2015 Akretion (http://www.akretion.com) # @author Alexis de Lattre # # This program is free software: you can redistribute it and/or modify diff --git a/sale_rental/__openerp__.py b/sale_rental/__openerp__.py index c45f6a0ec28..ee2358dcd7c 100644 --- a/sale_rental/__openerp__.py +++ b/sale_rental/__openerp__.py @@ -1,8 +1,8 @@ # -*- encoding: utf-8 -*- ############################################################################## # -# Rental module for OpenERP -# Copyright (C) 2014 Akretion (http://www.akretion.com) +# Rental module for Odoo +# Copyright (C) 2014-2015 Akretion (http://www.akretion.com) # @author Alexis de Lattre # # This program is free software: you can redistribute it and/or modify @@ -27,36 +27,6 @@ 'category': 'Sales Management', 'license': 'AGPL-3', 'summary': 'Manage Rental of Products', - 'description': """ -Rental -====== - -This module will allows you to rent products with OpenERP. On the form view of a stockable product or consumable, there is a wizard to generate the corresponding rental service. On the warehouse, you have two additionnal stock locations: *Rental In* (stock of products to rent) and *Rental Out* (products currently rented). - -In a sale order line (form view, not tree view), if you select a rental service, you can : - -* create a new rental with a start date and an end date: when the sale order is confirmed, it will generate a delivery order and an incoming shipment. - -* extend an existing rental: the incoming shipment will be postponed to the end date of the extension. - -In a sale order line, if you select a product that has a corresponding rental service, you can decide to sell the rented product that the customer already has. If the sale order is confirmed, the incoming shipment will be cancelled and a new delivery order will be created with a stock move from *Rental Out* to *Customers*. - -To use the module, you need to have access to the form view of sale order lines. For that, you must add your user to one of these groups: - -* Manage Product Packaging - -* Properties on lines - -A screencast that explains how to install, configure and use this module is available on Akretion's Youtube channel: https://www.youtube.com/watch?v=9o0QrGryBn8 - -Known limitations of the current implementation: - -* the unit of measure of the rental services must be *Day* (we don't support the rental per hours / per week / per month...) - -* when you sell a rental service, you must have as many sale order lines as rented equipements i.e. you can't rent multiple units of an equipment in one sale order line (it is possible to develop that, but it requires additionnal work). - -This module has been developped by Alexis de Lattre from Akretion . - """, 'author': 'Akretion', 'website': 'http://www.akretion.com', 'depends': ['sale_start_end_dates', 'stock'], diff --git a/sale_rental/i18n/sale_rental.pot b/sale_rental/i18n/sale_rental.pot index d737177b0c6..03e44582e6b 100644 --- a/sale_rental/i18n/sale_rental.pot +++ b/sale_rental/i18n/sale_rental.pot @@ -1,13 +1,13 @@ -# Translation of OpenERP Server. +# Translation of Odoo Server. # This file contains the translation of the following modules: # * sale_rental # msgid "" msgstr "" -"Project-Id-Version: OpenERP Server 7.0\n" +"Project-Id-Version: Odoo Server 8.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2014-02-02 22:36+0000\n" -"PO-Revision-Date: 2014-02-02 22:36+0000\n" +"POT-Creation-Date: 2015-02-26 11:07+0000\n" +"PO-Revision-Date: 2015-02-26 11:07+0000\n" "Last-Translator: <>\n" "Language-Team: \n" "MIME-Version: 1.0\n" @@ -16,223 +16,242 @@ msgstr "" "Plural-Forms: \n" #. module: sale_rental -#: code:addons/sale_rental/rental.py:83 -#, python-format -msgid "The unit of measure of the rental product '%s' must be 'Day'." +#: field:sale.order.line,can_sell_rental:0 +msgid "Can Sell from Rental" msgstr "" #. module: sale_rental -#: field:sale.order.line,sell_rental_id:0 -msgid "Rental to Sell" +#: code:addons/sale_rental/rental.py:517 +#, python-format +msgid "Can't find any generic 'Rent' route." msgstr "" #. module: sale_rental -#: selection:sale.order.line,rental_type:0 -msgid "New Rental" +#: code:addons/sale_rental/rental.py:529 +#, python-format +msgid "Can't find any generic 'Sell Rented Product' route." msgstr "" #. module: sale_rental -#: help:sale.rental,end_date:0 -msgid "End Date of the Rental, taking into account all the extensions sold to the customer." +#: view:create.rental.product:sale_rental.create_rental_product_form +msgid "Cancel" msgstr "" #. module: sale_rental -#: field:sale.order.line,rental_type:0 -msgid "Rental Type" +#: code:addons/sale_rental/rental.py:146 +#, python-format +msgid "Cannot sell the rental %s because it has already been returned" msgstr "" #. module: sale_rental -#: field:sale.rental,rented_product_id:0 -msgid "Rented Product" +#: code:addons/sale_rental/rental.py:151 +#, python-format +msgid "Cannot sell the rental %s because it has not been delivered" msgstr "" #. module: sale_rental -#: view:create.rental.product:0 +#: view:create.rental.product:sale_rental.create_rental_product_form msgid "Create" msgstr "" #. module: sale_rental -#: field:sale.order.line,extension_rental_id:0 -msgid "Rental to Extend" +#: view:create.rental.product:sale_rental.create_rental_product_form +#: model:ir.actions.act_window,name:sale_rental.create_rental_product_action +#: view:product.product:sale_rental.product_normal_form_view +msgid "Create Rental Service" msgstr "" #. module: sale_rental -#: field:sale.rental,out_state:0 -msgid "State of the Outgoing Stock Move" +#: model:ir.model,name:sale_rental.model_create_rental_product +msgid "Create the Rental Service Product" msgstr "" #. module: sale_rental -#: code:addons/sale_rental/rental.py:406 -#, python-format -msgid "The Rental Service of the Rental Extension you just selected is '%s' and it's not the same as the Product currently selected in this Sale Order Line." +#: field:create.rental.product,create_uid:0 +#: field:sale.rental,create_uid:0 +msgid "Created by" msgstr "" #. module: sale_rental -#: field:sale.rental,out_picking_id:0 -msgid "Delivery Order" +#: field:create.rental.product,create_date:0 +#: field:sale.rental,create_date:0 +msgid "Created on" msgstr "" #. module: sale_rental -#: field:sale.rental,in_picking_id:0 -msgid "Return Picking" +#: field:create.rental.product,default_code:0 +msgid "Default Code" msgstr "" #. module: sale_rental -#: view:sale.rental:0 -#: field:sale.rental,sell_order_line_ids:0 -msgid "Sell Rented Product" +#: view:sale.rental:sale_rental.sale_rental_form +#: view:sale.rental:sale_rental.sale_rental_tree +msgid "Delivery" msgstr "" #. module: sale_rental -#: field:sale.rental,in_state:0 -msgid "State of the Return Stock Move" +#: view:sale.rental:sale_rental.sale_rental_tree +msgid "End Date" msgstr "" #. module: sale_rental -#: model:ir.model,name:sale_rental.model_stock_picking -msgid "Picking List" +#: field:create.rental.product,id:0 +#: field:sale.rental,id:0 +msgid "ID" msgstr "" #. module: sale_rental -#: model:ir.model,name:sale_rental.model_stock_warehouse -msgid "Warehouse" +#: field:create.rental.product,write_uid:0 +#: field:sale.rental,write_uid:0 +msgid "Last Updated by" msgstr "" #. module: sale_rental -#: selection:sale.rental,in_state:0 -#: selection:sale.rental,out_state:0 -msgid "New" +#: field:create.rental.product,write_date:0 +#: field:sale.rental,write_date:0 +msgid "Last Updated on" msgstr "" #. module: sale_rental -#: view:sale.rental:0 -msgid "Unit of Measure" +#: code:addons/sale_rental/rental.py:183 +#, python-format +msgid "Missing Rental to Extend on the sale order line with rental service %s" msgstr "" #. module: sale_rental -#: selection:sale.rental,in_state:0 -#: selection:sale.rental,out_state:0 -msgid "Available" +#: code:addons/sale_rental/rental.py:142 +#, python-format +msgid "Missing return procurement on rental %s" msgstr "" #. module: sale_rental -#: model:stock.location,name:sale_rental.rental_out_stock_location -msgid "Rental Out" +#: selection:sale.order.line,rental_type:0 +msgid "New Rental" msgstr "" #. module: sale_rental -#: model:ir.model,name:sale_rental.model_product_product -msgid "Product" +#: code:addons/sale_rental/rental.py:304 +#, python-format +msgid "Not enough stock !" msgstr "" #. module: sale_rental -#: field:create.rental.product,categ_id:0 -msgid "Product Category" +#: code:addons/sale_rental/rental.py:199 +#, python-format +msgid "On the 'new rental' sale order line with product '%s', we should have a rental service product !" msgstr "" #. module: sale_rental -#: view:sale.rental:0 -msgid "Return" +#: code:addons/sale_rental/rental.py:212 +#, python-format +msgid "On the rental sale order line with product %sthe must have dates option should be enabled" msgstr "" #. module: sale_rental -#: code:addons/sale_rental/rental.py:59 +#: code:addons/sale_rental/rental.py:220 #, python-format -msgid "The rental product '%s' must have the option ''Must Have Start and End Dates' checked." +msgid "On the sale order line with product %s you are trying to sell a rented product with a quantity (%s) that is different from the rented quantity (%s). This is not supported." msgstr "" #. module: sale_rental -#: field:sale.rental,out_move_id:0 -msgid "Outgoing Stock Move" +#: code:addons/sale_rental/rental.py:204 +#, python-format +msgid "On the sale order line with product '%s' the Product Quantity (%s) should be the number of days (%s) multiplied by the Rental Quantity (%s)." msgstr "" #. module: sale_rental -#: field:sale.rental,company_id:0 -msgid "Company" +#: code:addons/sale_rental/rental.py:188 +#, python-format +msgid "On the sale order line with rental service %s, you are trying to extend a rental with a rental quantity (%s) that is different from the quantity of the original rental (%s). This is not supported." msgstr "" #. module: sale_rental -#: view:sale.rental:0 -msgid "Delivery" +#: model:ir.model,name:sale_rental.model_product_product +msgid "Product" msgstr "" #. module: sale_rental -#: field:sale.rental,prodlot_id:0 -msgid "Serial Number" +#: field:create.rental.product,categ_id:0 +msgid "Product Category" msgstr "" #. module: sale_rental -#: field:sale.rental,end_date:0 -msgid "End Date (extensions included)" +#: field:create.rental.product,name:0 +msgid "Product Name" msgstr "" #. module: sale_rental -#: selection:sale.rental,in_state:0 -#: selection:sale.rental,out_state:0 -msgid "Waiting Availability" +#: code:addons/sale_rental/wizard/create_rental_product.py:45 +#, python-format +msgid "RENT-%s" msgstr "" #. module: sale_rental -#: model:ir.model,name:sale_rental.model_create_rental_product -msgid "Create the Rental Service Product" +#: view:product.product:sale_rental.product_normal_form_view +#: field:product.product,rental_service_ids:0 +msgid "Related Rental Services" msgstr "" #. module: sale_rental -#: field:stock.warehouse,rental_out_location_id:0 -msgid "Rental Output" +#: field:product.product,rented_product_id:0 +msgid "Related Rented Product" +msgstr "" + +#. module: sale_rental +#: code:addons/sale_rental/rental.py:514 +#, python-format +msgid "Rent" msgstr "" #. module: sale_rental #: model:ir.model,name:sale_rental.model_sale_rental -#: view:product.product:0 +#: view:product.product:sale_rental.product_normal_form_view #: field:sale.order.line,rental:0 -#: view:sale.rental:0 +#: view:sale.rental:sale_rental.sale_rental_form msgid "Rental" msgstr "" #. module: sale_rental -#: field:product.product,rented_product_id:0 -msgid "Related Rented Product" +#: field:stock.warehouse,rental_allowed:0 +msgid "Rental Allowed" msgstr "" #. module: sale_rental -#: field:sale.rental,start_order_id:0 -msgid "Rental Sale Order" +#: selection:sale.order.line,rental_type:0 +msgid "Rental Extension" msgstr "" #. module: sale_rental -#: view:sale.rental:0 -msgid "End Date" +#: view:sale.rental:sale_rental.sale_rental_form +#: field:sale.rental,extension_order_line_ids:0 +msgid "Rental Extensions" msgstr "" #. module: sale_rental +#: model:stock.location,name:sale_rental.rental_in_stock_location #: field:stock.warehouse,rental_in_location_id:0 -msgid "Rental Input" +msgid "Rental In" msgstr "" #. module: sale_rental -#: constraint:product.product:0 -msgid "error msg in raise" +#: model:stock.location,name:sale_rental.rental_out_stock_location +#: field:stock.warehouse,rental_out_location_id:0 +msgid "Rental Out" msgstr "" #. module: sale_rental -#: field:create.rental.product,name_prefix:0 -msgid "Product Name Prefix" +#: field:create.rental.product,sale_price_per_day:0 +msgid "Rental Price per Day" msgstr "" #. module: sale_rental -#: code:addons/sale_rental/rental.py:53 -#: code:addons/sale_rental/rental.py:58 -#: code:addons/sale_rental/rental.py:82 -#: code:addons/sale_rental/rental.py:220 -#, python-format -msgid "Error:" +#: field:sale.order.line,rental_qty:0 +msgid "Rental Quantity" msgstr "" #. module: sale_rental -#: view:sale.rental:0 -#: field:sale.rental,extension_order_line_ids:0 -msgid "Rental Extensions" +#: field:stock.warehouse,rental_route_id:0 +msgid "Rental Route" msgstr "" #. module: sale_rental @@ -241,129 +260,141 @@ msgid "Rental Sale Order Line" msgstr "" #. module: sale_rental -#: field:sale.rental,in_move_id:0 -msgid "Return Stock Move" +#: field:sale.order.line,rental_type:0 +msgid "Rental Type" msgstr "" #. module: sale_rental -#: code:addons/sale_rental/rental.py:221 +#: code:addons/sale_rental/wizard/create_rental_product.py:37 #, python-format -msgid "Missing Rental Extension for Sale Order Line with description '%s'" +msgid "Rental of one %s" msgstr "" #. module: sale_rental -#: field:create.rental.product,default_code_prefix:0 -msgid "Prefix for Default Code" +#: model:product.template,name:sale_rental.rent_product_product_25_product_template +msgid "Rental of one Laptop E5023" msgstr "" #. module: sale_rental -#: code:addons/sale_rental/rental.py:405 -#, python-format -msgid "Error" +#: model:product.template,name:sale_rental.rent_product_product_8_product_template +msgid "Rental of one iMac" msgstr "" #. module: sale_rental -#: selection:sale.rental,in_state:0 -#: selection:sale.rental,out_state:0 -msgid "Cancelled" +#: model:product.template,name:sale_rental.rent_product_product_6_product_template +msgid "Rental of one iPad Mini" msgstr "" #. module: sale_rental -#: selection:sale.order.line,rental_type:0 -msgid "Rental Extension" +#: field:sale.order.line,extension_rental_id:0 +msgid "Rental to Extend" +msgstr "" + +#. module: sale_rental +#: field:sale.order.line,sell_rental_id:0 +msgid "Rental to Sell" msgstr "" #. module: sale_rental #: model:ir.actions.act_window,name:sale_rental.sale_rental_action #: model:ir.ui.menu,name:sale_rental.sale_rental_menu -#: view:sale.rental:0 -#: field:stock.move,sale_rental_ids:0 +#: view:sale.rental:sale_rental.sale_rental_tree msgid "Rentals" msgstr "" #. module: sale_rental -#: field:sale.order.line,can_sell_rental:0 -msgid "Can Sell from Rental" +#: view:sale.rental:sale_rental.sale_rental_form +#: view:sale.rental:sale_rental.sale_rental_tree +msgid "Return" msgstr "" #. module: sale_rental -#: selection:sale.rental,in_state:0 -#: selection:sale.rental,out_state:0 -msgid "Waiting Another Move" +#: model:ir.model,name:sale_rental.model_sale_order +msgid "Sales Order" msgstr "" #. module: sale_rental -#: view:sale.rental:0 -msgid "Total" +#: model:ir.model,name:sale_rental.model_sale_order_line +msgid "Sales Order Line" msgstr "" #. module: sale_rental -#: field:sale.rental,rental_product_id:0 -msgid "Rental Service" +#: code:addons/sale_rental/rental.py:523 +#: view:sale.rental:sale_rental.sale_rental_form +#: field:sale.rental,sell_order_line_ids:0 +#, python-format +msgid "Sell Rented Product" msgstr "" #. module: sale_rental -#: field:create.rental.product,sale_price_per_day:0 -msgid "Sale Price per Day" +#: field:stock.warehouse,sell_rented_product_route_id:0 +msgid "Sell Rented Product Route" msgstr "" #. module: sale_rental -#: code:addons/sale_rental/rental.py:54 +#: model:ir.model,name:sale_rental.model_stock_move +msgid "Stock Move" +msgstr "" + +#. module: sale_rental +#: code:addons/sale_rental/rental.py:533 #, python-format -msgid "The rental product '%s' must be of type 'Service'." +msgid "The Rental Input stock location is not set on the warehouse %s" msgstr "" #. module: sale_rental -#: model:ir.model,name:sale_rental.model_stock_move -msgid "Stock Move" +#: code:addons/sale_rental/rental.py:537 +#, python-format +msgid "The Rental Output stock location is not set on the warehouse %s" msgstr "" #. module: sale_rental -#: view:product.product:0 -#: field:product.product,rental_service_ids:0 -msgid "Related Rental Services" +#: code:addons/sale_rental/rental.py:326 +#, python-format +msgid "The Rental Service of the Rental Extension you just selected is '%s' and it's not the same as the Product currently selected in this Sale Order Line." msgstr "" #. module: sale_rental -#: view:create.rental.product:0 -#: model:ir.actions.act_window,name:sale_rental.create_rental_product_action -#: view:product.product:0 -msgid "Create Rental Service" +#: code:addons/sale_rental/rental.py:51 +#, python-format +msgid "The rental product '%s' must be of type 'Service'." msgstr "" #. module: sale_rental -#: selection:sale.rental,in_state:0 -#: selection:sale.rental,out_state:0 -msgid "Done" +#: code:addons/sale_rental/rental.py:55 +#, python-format +msgid "The rental product '%s' must have the option ''Must Have Start and End Dates' checked." msgstr "" #. module: sale_rental -#: model:stock.location,name:sale_rental.rental_in_stock_location -msgid "Rental In" +#: code:addons/sale_rental/rental.py:74 +#, python-format +msgid "The unit of measure of the rental product '%s' must be 'Day'." msgstr "" #. module: sale_rental -#: view:create.rental.product:0 -msgid "Cancel" +#: view:sale.rental:sale_rental.sale_rental_form +msgid "Total" msgstr "" #. module: sale_rental -#: field:sale.rental,partner_id:0 -msgid "Partner" +#: view:sale.rental:sale_rental.sale_rental_form +msgid "Unit of Measure" msgstr "" #. module: sale_rental -#: field:sale.rental,start_date:0 -msgid "Start Date" +#: model:ir.model,name:sale_rental.model_stock_warehouse +msgid "Warehouse" msgstr "" #. module: sale_rental -#: model:ir.model,name:sale_rental.model_sale_order -msgid "Sales Order" +#: code:addons/sale_rental/rental.py:306 +#, python-format +msgid "You want to rent %.2f %s but you only have %.2f %s currently available on the stock location '%s' ! Make sure that you get some units back in the mean time or re-supply the stock location '%s'." msgstr "" #. module: sale_rental -#: model:ir.model,name:sale_rental.model_sale_order_line -msgid "Sales Order Line" +#: view:sale.order:sale_rental.view_order_form_sale_stock +msgid "product_uom_qty_change_with_wh_with_rental(parent.pricelist_id,product_id,product_uom_qty,product_uom,product_uos_qty,False,name,parent.partner_id, False, False, parent.date_order, product_packaging, parent.fiscal_position, True, parent.warehouse_id, rental_type, rental_qty, context)" msgstr "" diff --git a/sale_rental/product_view.xml b/sale_rental/product_view.xml index 3a4bf03c31f..2cbf352c485 100644 --- a/sale_rental/product_view.xml +++ b/sale_rental/product_view.xml @@ -1,7 +1,7 @@ @@ -9,6 +9,7 @@ + rental.product.product.form product.product diff --git a/sale_rental/rental.py b/sale_rental/rental.py index 1b6f68faf84..591ccbb8a56 100644 --- a/sale_rental/rental.py +++ b/sale_rental/rental.py @@ -1,8 +1,8 @@ # -*- encoding: utf-8 -*- ############################################################################## # -# Sale Rental module for OpenERP -# Copyright (C) 2014 Akretion (http://www.akretion.com) +# Sale Rental module for Odoo +# Copyright (C) 2014-2015 Akretion (http://www.akretion.com) # @author Alexis de Lattre # # This program is free software: you can redistribute it and/or modify @@ -20,362 +20,222 @@ # ############################################################################## -from openerp.osv import orm, fields -from openerp.tools.translate import _ -from openerp import netsvc -from datetime import datetime, timedelta -from openerp.tools import DEFAULT_SERVER_DATE_FORMAT -from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT +from openerp import models, fields, api, _ +from openerp.exceptions import Warning, ValidationError +from openerp.tools import float_compare from dateutil.relativedelta import relativedelta +import openerp.addons.decimal_precision as dp import logging logger = logging.getLogger(__name__) # TODO : block if we sell a rented product already sold => state -class product_product(orm.Model): +class ProductProduct(models.Model): _inherit = 'product.product' - _columns = { - # Link rental service -> rented HW product - 'rented_product_id': fields.many2one( - 'product.product', 'Related Rented Product', - domain=[('type', 'in', ('product', 'consu'))]), - # Link rented HW product -> rental service - 'rental_service_ids': fields.one2many( - 'product.product', 'rented_product_id', 'Related Rental Services'), - } - - def _check_rental(self, cr, uid, ids): - for product in self.browse(cr, uid, ids): - if product.rented_product_id and product.type != 'service': - raise orm.except_orm( - _("Error:"), - _("The rental product '%s' must be of type 'Service'.") - % product.name) - if product.rented_product_id and not product.must_have_dates: - raise orm.except_orm( - _("Error:"), - _("The rental product '%s' must have the option " - "''Must Have Start and End Dates' checked.") - % product.name) - # In the future, we would like to support all time UoMs - # but it is more complex and requires additionnal developments - #(model, time_uom_categ_id) = self.pool['ir.model.data'].\ - # get_object_reference(cr, uid, 'product', 'uom_categ_wtime') - #assert model == 'product.uom.categ', 'Must be a product uom categ' - #if ( - # product.rented_product_id and - # product.uom_id.category_id.id != time_uom_categ_id): - # raise orm.except_orm( - # _("Error:"), - # _("The rental product '%s' must have a unit of measure " - # "that belong to the 'Time' category.") - # % product.name) - (model, day_uom_id) = self.pool['ir.model.data'].\ - get_object_reference(cr, uid, 'product', 'product_uom_day') - assert model == 'product.uom', 'Must be a product uom' - if ( - product.rented_product_id and - product.uom_id.id != day_uom_id): - raise orm.except_orm( - _("Error:"), - _("The unit of measure of the rental product '%s' must " - "be 'Day'.") - % product.name) - return True - - _constraints = [( - _check_rental, "error msg in raise", - ['rented_product_id', 'must_have_dates'] - )] - - -class sale_order(orm.Model): + # Link rental service -> rented HW product + rented_product_id = fields.Many2one( + 'product.product', string='Related Rented Product', + domain=[('type', 'in', ('product', 'consu'))]) + # Link rented HW product -> rental service + rental_service_ids = fields.One2many( + 'product.product', 'rented_product_id', + string='Related Rental Services') + + @api.one + @api.constrains('rented_product_id', 'must_have_dates') + def _check_rental(self): + if self.rented_product_id and self.type != 'service': + raise ValidationError( + _("The rental product '%s' must be of type 'Service'.") + % self.name) + if self.rented_product_id and not self.must_have_dates: + raise ValidationError( + _("The rental product '%s' must have the option " + "''Must Have Start and End Dates' checked.") + % self.name) + # In the future, we would like to support all time UoMs + # but it is more complex and requires additionnal developments + day_uom = self.env.ref('product.product_uom_day') + if self.rented_product_id and self.uom_id != day_uom: + raise ValidationError( + _("The unit of measure of the rental product '%s' must " + "be 'Day'.") + % self.name) + + +class SaleOrder(models.Model): _inherit = 'sale.order' - def _prepare_order_line_procurement( - self, cr, uid, order, line, move_id, date_planned, context=None): - if context is None: - context = {} - res = super(sale_order, self)._prepare_order_line_procurement( - cr, uid, order, line, move_id, date_planned, context=context) - if context.get('rent') == 'out': - res.update({ - 'product_id': line.product_id.rented_product_id.id, - 'product_qty': 1, - 'product_uos_qty': 1, - 'product_uom': line.product_id.rented_product_id.uom_id.id, - 'product_uos': line.product_id.rented_product_id.uom_id.id, - 'location_id': - order.shop_id.warehouse_id.rental_in_location_id.id, - #'procure_method': , #????? - }) - return res + @api.model + def _get_rental_date_planned(self, line): + return line.start_date - def _prepare_order_line_move( - self, cr, uid, order, line, picking_id, date_planned, - context=None): - if context is None: - context = {} - res = super(sale_order, self)._prepare_order_line_move( - cr, uid, order, line, picking_id, date_planned, context=context) - rent_type = context.get('rent') - if rent_type: - loc_rent_in = order.shop_id.warehouse_id.rental_in_location_id.id - loc_rent_out = order.shop_id.warehouse_id.rental_out_location_id.id - if rent_type == 'out': - location_id = loc_rent_in - location_dest_id = loc_rent_out - elif rent_type == 'in': - location_id = loc_rent_out - location_dest_id = loc_rent_in + @api.model + def _prepare_order_line_procurement( + self, order, line, group_id=False): + res = super(SaleOrder, self)._prepare_order_line_procurement( + order, line, group_id=group_id) + if ( + line.product_id.rented_product_id + and line.rental_type == 'new_rental'): res.update({ 'product_id': line.product_id.rented_product_id.id, - 'product_qty': 1, - 'product_uos_qty': 1, + 'product_qty': line.rental_qty, + 'product_uos_qty': line.rental_qty, 'product_uom': line.product_id.rented_product_id.uom_id.id, 'product_uos': line.product_id.rented_product_id.uom_id.id, - 'location_id': location_id, - 'location_dest_id': location_dest_id, - 'product_packaging': ( - line.product_id.rented_product_id.packaging - and line.product_id.rented_product_id.packaging[0].id - or False), - 'price_unit': - line.product_id.rented_product_id.standard_price or 0.0, - 'move_dest_id': context.get('in_move_id', False), - }) - if line.sell_rental_id: - res.update({ - 'prodlot_id': line.sell_rental_id.prodlot_id.id, 'location_id': - order.shop_id.warehouse_id.rental_out_location_id.id, + order.warehouse_id.rental_out_location_id.id, + 'route_ids': + [(6, 0, [line.order_id.warehouse_id.rental_route_id.id])], + 'date_planned': self._get_rental_date_planned(line), }) + elif line.sell_rental_id: + res['route_ids'] = [(6, 0, [ + line.order_id.warehouse_id.sell_rented_product_route_id.id])] return res - def _prepare_order_picking_rent_in( - self, cr, uid, order, date, context=None): - return { - 'name': self.pool['ir.sequence'].get(cr, uid, 'stock.picking.in'), - 'origin': order.name, - 'date': self.date_to_datetime(cr, uid, date, context), - 'type': 'in', - 'state': 'auto', - 'move_type': order.picking_policy, - 'sale_id': order.id, - 'partner_id': order.partner_shipping_id.id, - 'note': order.note, - 'invoice_state': 'none', - 'company_id': order.company_id.id, - } - - def _prepare_rental(self, cr, uid, line, out_move_id, context=None): - return { - 'start_order_line_id': line.id, - 'out_move_id': out_move_id, - } - - def _get_date_planned( - self, cr, uid, order, line, start_date, context=None): - '''We inherit this function because we want to have the - date_planned of the regular products that are on the same SO - as the rentals = the start date of the rentals, - because we suppose that these products are "accessories" - of the rental.''' - if context is None: - context = {} - rental_start_date_planned = context.get('rental_start_date_planned') - if rental_start_date_planned: - return rental_start_date_planned - else: - return super(sale_order, self)._get_date_planned( - cr, uid, order, line, start_date, context=context) - - def _create_pickings_and_procurements( - self, cr, uid, order, order_lines, picking_id=False, context=None): - picking_obj = self.pool['stock.picking'] - proc_obj = self.pool['procurement.order'] - move_obj = self.pool['stock.move'] - wf_service = netsvc.LocalService("workflow") - if context is None: - context = {} - picking_out_id = False - picking_in_dict = {} # key = date ; value = ID - proc_ids = [] - rent_out_ctx = context.copy() - rent_out_ctx['rent'] = 'out' - rent_in_ctx = context.copy() - rent_in_ctx['rent'] = 'in' - for line in order_lines: - if line.sell_rental_id: - # Cancel return picking - wf_service.trg_validate( - uid, 'stock.picking', line.sell_rental_id.in_picking_id.id, - 'button_cancel', cr) - if line.product_id.rented_product_id: - if line.rental_type == 'rental_extension': - if not line.extension_rental_id: - raise orm.except_orm( - _('Error:'), - _("Missing Rental Extension for Sale Order Line " - "with description '%s'") - % line.name) - new_datetime = self.date_to_datetime( - cr, uid, line.end_date, context) - move_obj.write( - cr, uid, line.extension_rental_id.in_move_id.id, { - 'date': new_datetime, - 'date_expected': new_datetime, - }, context=context) - picking_obj.write( - cr, uid, line.extension_rental_id.in_picking_id.id, { - 'date': line.end_date, - }, context=context) - elif line.rental_type == 'new_rental': - # No pb to keep the native code, unless for date - # but if we have both in the same... - - # Create return picking - if line.end_date not in picking_in_dict: - picking_in_id = picking_obj.create( - cr, uid, self._prepare_order_picking_rent_in( - cr, uid, order, line.end_date, - context=context), - context=context) - picking_in_dict[line.end_date] = picking_in_id - else: - picking_in_id = picking_in_dict[line.end_date] - - end_datetime = self.date_to_datetime( - cr, uid, line.end_date, context) - - in_move_id = move_obj.create( - cr, uid, self._prepare_order_line_move( - cr, uid, order, line, picking_in_id, - end_datetime, context=rent_in_ctx), - context=context) - - # Create outgoing picking - # TODO : make start_date_planned inheritable - start_datetime_str = self.date_to_datetime( - cr, uid, line.start_date, context) - start_datetime = datetime.strptime( - start_datetime_str, DEFAULT_SERVER_DATETIME_FORMAT) - start_date_planned = ( - start_datetime - timedelta( - days=order.company_id.security_lead)).strftime( - DEFAULT_SERVER_DATETIME_FORMAT) - context['rental_start_date_planned'] = start_date_planned - - if not picking_out_id: - picking_out_id = picking_obj.create( - cr, uid, self._prepare_order_picking( - cr, uid, order, context=rent_out_ctx), - context=context) - - rent_out_ctx['in_move_id'] = in_move_id - out_move_id = move_obj.create( - cr, uid, self._prepare_order_line_move( - cr, uid, order, line, picking_out_id, - start_date_planned, context=rent_out_ctx), - context=context) - - # Create outgoing procurement - proc_id = proc_obj.create( - cr, uid, self._prepare_order_line_procurement( - cr, uid, order, line, out_move_id, - start_date_planned, context=rent_out_ctx), - context=context) - proc_ids.append(proc_id) - line.write({'procurement_id': proc_id}) -# self.ship_recreate( -# cr, uid, order, line, out_move_id, proc_id) - - self.pool['sale.rental'].create( - cr, uid, self._prepare_rental( - cr, uid, line, out_move_id, context=context), - context=context) - - for proc_id in proc_ids: - wf_service.trg_validate( - uid, 'procurement.order', proc_id, 'button_confirm', cr) - for picking_in_id in picking_in_dict.values(): - wf_service.trg_validate( - uid, 'stock.picking', picking_in_id, 'button_confirm', cr) - res = super(sale_order, self)._create_pickings_and_procurements( - cr, uid, order, order_lines, picking_id=picking_out_id, - context=context) + @api.model + def _prepare_rental(self, so_line): + return {'start_order_line_id': so_line.id} + + @api.multi + def action_button_confirm(self): + res = super(SaleOrder, self).action_button_confirm() + for order in self: + for line in order.order_line: + if line.rental_type == 'new_rental': + self.env['sale.rental'].create(self._prepare_rental(line)) + elif line.rental_type == 'rental_extension': + line.extension_rental_id.in_move_id.date_expected =\ + line.end_date + line.extension_rental_id.in_move_id.date = line.end_date + elif line.sell_rental_id: + if line.sell_rental_id.out_move_id.state != 'done': + raise Warning( + _('Cannot sell the rental %s because it has ' + 'not been delivered') + % line.sell_rental_id.display_name) + line.sell_rental_id.in_move_id.action_cancel() return res -class sale_order_line(orm.Model): +class SaleOrderLine(models.Model): _inherit = 'sale.order.line' - _columns = { - 'rental': fields.boolean('Rental'), - 'can_sell_rental': fields.boolean('Can Sell from Rental'), - 'rental_type': fields.selection([ - ('new_rental', 'New Rental'), - ('rental_extension', 'Rental Extension'), - ], 'Rental Type', - readonly=True, states={'draft': [('readonly', False)]}), - 'extension_rental_id': fields.many2one( - 'sale.rental', 'Rental to Extend'), - 'sell_rental_id': fields.many2one( - 'sale.rental', 'Rental to Sell'), - # TODO : related one2many + impact sur rental - } - - def start_end_dates_change( - self, cr, uid, ids, start_date, end_date, product_id, - product_uom_qty, context=None): - res = super(sale_order_line, self).start_end_dates_change( - cr, uid, ids, start_date, end_date, product_id, - product_uom_qty, context=context) - if 'value' not in res: - res['value'] = {} - if start_date and end_date and product_id: - product = self.pool['product.product'].browse( - cr, uid, product_id, context=context) - if product.rented_product_id: - start_date_dt = datetime.strptime( - start_date, DEFAULT_SERVER_DATE_FORMAT) - end_date_dt = datetime.strptime( - end_date, DEFAULT_SERVER_DATE_FORMAT) - days_qty = (end_date_dt - start_date_dt).days + 1 - res['value']['product_uom_qty'] = days_qty + rental = fields.Boolean(string='Rental') + can_sell_rental = fields.Boolean(string='Can Sell from Rental') + rental_type = fields.Selection([ + ('new_rental', 'New Rental'), + ('rental_extension', 'Rental Extension'), + ], 'Rental Type', + readonly=True, states={'draft': [('readonly', False)]}) + extension_rental_id = fields.Many2one( + 'sale.rental', string='Rental to Extend') + rental_qty = fields.Float( + string='Rental Quantity', digits=dp.get_precision('Product UoS')) + sell_rental_id = fields.Many2one( + 'sale.rental', string='Rental to Sell') + + @api.one + @api.constrains( + 'rental_type', 'extension_rental_id', 'start_date', 'end_date', + 'rental_qty', 'product_uom_qty', 'product_id', 'must_have_dates') + def _check_sale_line_rental(self): + if self.rental_type == 'rental_extension': + if not self.extension_rental_id: + raise ValidationError( + _("Missing Rental to Extend on the sale order line with " + "rental service %s") + % self.product_id.name) + if self.rental_qty != self.extension_rental_id.rental_qty: + raise ValidationError( + _("On the sale order line with rental service %s, " + "you are trying to extend a rental with a rental " + "quantity (%s) that is different from the quantity " + "of the original rental (%s). This is not supported.") + % ( + self.product_id.name, + self.rental_qty, + self.extension_rental_id.rental_qty)) + if self.rental_type in ('new_rental', 'rental_extension'): + if not self.product_id.rented_product_id: + raise ValidationError( + _("On the 'new rental' sale order line with product '%s', " + "we should have a rental service product !") % ( + self.product_id.name)) + if self.product_uom_qty != self.rental_qty * self.number_of_days: + raise ValidationError( + _("On the sale order line with product '%s' " + "the Product Quantity (%s) should be the " + "number of days (%s) " + "multiplied by the Rental Quantity (%s).") % ( + self.product_id.name, self.product_uom_qty, + self.number_of_days, self.rental_qty)) + if not self.must_have_dates: + raise ValidationError( + _("On the rental sale order line with product %s" + "the must have dates option should be enabled") + % self.product_id.name) + # the module sale_start_end_dates checks that, when we have + # must_have_dates, we have start + end dates + elif self.sell_rental_id: + if self.product_uom_qty != self.sell_rental_id.rental_qty: + raise ValidationError( + _("On the sale order line with product %s " + "you are trying to sell a rented product with a " + "quantity (%s) that is different from the rented " + "quantity (%s). This is not supported.") % ( + self.product_id.name, + self.product_uom_qty, + self.sell_rental_id.rental_qty)) + + @api.multi + def need_procurement(self): + res = super(SaleOrderLine, self).need_procurement() + if not res: + for soline in self: + if ( + soline.product_id.rented_product_id + and soline.rental_type == 'new_rental'): + return True return res - def product_id_change( - self, cr, uid, ids, pricelist, product, qty=0, + @api.multi + def product_id_change_with_wh( + self, pricelist, product, qty=0, uom=False, qty_uos=0, uos=False, name='', partner_id=False, lang=False, update_tax=True, date_order=False, packaging=False, - fiscal_position=False, flag=False, context=None): - res = super(sale_order_line, self).product_id_change( - cr, uid, ids, pricelist, product, qty=qty, uom=uom, + fiscal_position=False, flag=False, warehouse_id=False): + res = super(SaleOrderLine, self).product_id_change_with_wh( + pricelist, product, qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name, partner_id=partner_id, lang=lang, update_tax=update_tax, date_order=date_order, packaging=packaging, fiscal_position=fiscal_position, - flag=flag, context=context) + flag=flag, warehouse_id=warehouse_id) if not product: res['value'].update({ 'rental_type': False, 'rental': False, + 'rental_qty': 0, 'can_sell_rental': False, 'sell_rental_id': False, }) else: - product_o = self.pool['product.product'].browse( - cr, uid, product, context=context) + product_o = self.env['product.product'].browse(product) if product_o.rented_product_id: - res['value'].update({ - 'rental': True, - }) + res['value']['rental'] = True + # We can't set rental_type to default value 'new_rental' here + # because we would need to check if rental_type is False + # and we don't have rental_type as arg of + # product_id_change_with_wh() else: res['value'].update({ 'rental_type': False, 'rental': False, + 'rental_qty': 0, }) if product_o.rental_service_ids: res['value']['can_sell_rental'] = True @@ -386,194 +246,397 @@ def product_id_change( }) return res - def rental_type_change( - self, cr, uid, ids, rental_type, product_id, context=None): - return {} + @api.multi + def product_uom_qty_change_with_wh_with_rental( + self, pricelist, product, qty=0, + uom=False, qty_uos=0, uos=False, name='', partner_id=False, + lang=False, update_tax=True, date_order=False, packaging=False, + fiscal_position=False, flag=False, warehouse_id=False, + rental_type=False, rental_qty=0): + res = super(SaleOrderLine, self).product_id_change_with_wh( + pricelist, product, qty=qty, uom=uom, + qty_uos=qty_uos, uos=uos, name=name, partner_id=partner_id, + lang=lang, update_tax=update_tax, date_order=date_order, + packaging=packaging, fiscal_position=fiscal_position, + flag=flag, warehouse_id=warehouse_id) + if ( + product + and rental_type == 'new_rental' + and rental_qty + and warehouse_id): + product_o = self.env['product.product'].browse(product) + if product_o.rented_product_id: + product_uom = product_o.rented_product_id.uom_id + warehouse = self.env['stock.warehouse'].browse(warehouse_id) + rental_in_location = warehouse.rental_in_location_id + product_o_in = self.with_context( + location=rental_in_location.id).env['product.product']\ + .browse(product) + in_location_available_qty =\ + product_o_in.rented_product_id.qty_available\ + - product_o_in.rented_product_id.outgoing_qty + compare_qty = float_compare( + in_location_available_qty, + rental_qty, precision_rounding=product_uom.rounding) + if compare_qty == -1: + res['warning'] = { + 'title': _("Not enough stock !"), + 'message': + _("You want to rent %.2f %s but you only " + "have %.2f %s currently available on the stock " + "location '%s' ! Make sure that you get some " + "units back in the mean time or re-supply the " + "stock location '%s'.") + % (rental_qty, product_uom.name, + in_location_available_qty, + product_uom.name, rental_in_location.name, + rental_in_location.name) + } + return res - def extension_rental_id_change( - self, cr, uid, ids, rental_type, extension_rental_id, - product_id, start_date, end_date, context=None): - res = {'value': {}} + @api.onchange('extension_rental_id') + def extension_rental_id_change(self): if ( - product_id - and rental_type == 'rental_extension' - and extension_rental_id): - rental_ext = self.pool['sale.rental'].browse( - cr, uid, extension_rental_id, context=context) - if rental_ext.rental_product_id.id != product_id: - raise orm.except_orm( - _('Error'), + self.product_id + and self.rental_type == 'rental_extension' + and self.extension_rental_id): + if self.extension_rental_id.rental_product_id != self.product_id: + raise Warning( _("The Rental Service of the Rental Extension you just " "selected is '%s' and it's not the same as the " "Product currently selected in this Sale Order Line.") - % rental_ext.rental_product_id.name) - initial_end_date_str = rental_ext.end_date - initial_end_date = datetime.strptime( - initial_end_date_str, DEFAULT_SERVER_DATE_FORMAT) - start_date = initial_end_date + relativedelta(days=1) - res['value']['start_date'] = start_date.strftime( - DEFAULT_SERVER_DATE_FORMAT) - return res - - -class sale_rental(orm.Model): + % self.extension_rental_id.rental_product_id.name) + initial_end_date = fields.Date.from_string( + self.extension_rental_id.end_date) + self.start_date = initial_end_date + relativedelta(days=1) + self.rental_qty = self.extension_rental_id.rental_qty + + @api.onchange('sell_rental_id') + def sell_rental_id_change(self): + if self.sell_rental_id: + self.product_uom_qty = self.sell_rental_id.rental_qty + self.product_uos_qty = self.sell_rental_id.rental_qty + + @api.onchange('rental_qty', 'number_of_days') + def rental_qty_number_of_days_change(self): + qty = self.rental_qty * self.number_of_days + self.product_uom_qty = qty + self.product_uos_qty = qty + + +class SaleRental(models.Model): _name = 'sale.rental' _description = "Rental" _order = "id desc" + _rec_name = "display_name" + + @api.one + @api.depends( + 'start_order_line_id', 'extension_order_line_ids.end_date', + 'extension_order_line_ids.state', 'start_order_line_id.end_date') + def _display_name(self): + self.display_name = u'[%s] %s - %s > %s (%s)' % ( + self.partner_id.name, + self.rented_product_id.name, + self.start_date, + self.end_date, + self.state) # TODO : display label, not the technical key + + @api.one + @api.depends('start_order_line_id', 'start_order_line_id.procurement_ids') + def _compute_procurement_and_move(self): + procurement = False + in_move = False + out_move = False + sell_procurement = False + sell_move = False + state = False + if ( + self.start_order_line_id + and self.start_order_line_id.procurement_ids): + + procurement = self.start_order_line_id.procurement_ids[0] + if procurement.move_ids: + for move in procurement.move_ids: + if move.move_dest_id: + out_move = move + else: + in_move = move + if ( + self.sell_order_line_ids and + self.sell_order_line_ids[0].procurement_ids): + sell_procurement =\ + self.sell_order_line_ids[0].procurement_ids[0] + if sell_procurement.move_ids: + sell_move = sell_procurement.move_ids[0] + state = 'ordered' + if out_move and in_move: + if out_move.state == 'done': + state = 'out' + if out_move.state == 'done' and in_move.state == 'done': + state = 'in' + if ( + out_move.state == 'done' + and in_move.state == 'cancel' + and sell_procurement): + state = 'sell_progress' + if sell_move and sell_move.state == 'done': + state = 'sold' + self.procurement_id = procurement + self.in_move_id = in_move + self.out_move_id = out_move + self.state = state + self.sell_procurement_id = sell_procurement + self.sell_move_id = sell_move + + @api.one + @api.depends( + 'extension_order_line_ids.end_date', 'extension_order_line_ids.state', + 'start_order_line_id.end_date') + def _compute_end_date(self): + end_date = False + if self.extension_order_line_ids: + for extension in self.extension_order_line_ids: + if extension.state not in ('cancel', 'draft'): + if extension.end_date > end_date: + end_date = extension.end_date + if not end_date and self.start_order_line_id: + end_date = self.start_order_line_id.end_date + self.end_date = end_date + + display_name = fields.Char( + compute='_display_name', string='Display Name') + start_order_line_id = fields.Many2one( + 'sale.order.line', string='Rental Sale Order Line') + start_date = fields.Date( + related='start_order_line_id.start_date', + string='Start Date', readonly=True) + rental_product_id = fields.Many2one( + 'product.product', related='start_order_line_id.product_id', + string="Rental Service", readonly=True) + rented_product_id = fields.Many2one( + 'product.product', + related='start_order_line_id.product_id.rented_product_id', + string="Rented Product", readonly=True) + rental_qty = fields.Float( + related='start_order_line_id.rental_qty', + string="Rental Quantity", readonly=True) + start_order_id = fields.Many2one( + 'sale.order', related='start_order_line_id.order_id', + string='Rental Sale Order', readonly=True) + company_id = fields.Many2one( + 'res.company', related='start_order_id.company_id', + string='Company', readonly=True) + partner_id = fields.Many2one( + 'res.partner', related='start_order_id.partner_id', + string='Partner', readonly=True) + procurement_id = fields.Many2one( + 'procurement.order', string="Procurement", readonly=True, + compute='_compute_procurement_and_move') + out_move_id = fields.Many2one( + 'stock.move', compute='_compute_procurement_and_move', + string='Outgoing Stock Move', readonly=True) + in_move_id = fields.Many2one( + 'stock.move', compute='_compute_procurement_and_move', + string='Return Stock Move', readonly=True) + out_state = fields.Selection([ + ('draft', 'New'), + ('cancel', 'Cancelled'), + ('waiting', 'Waiting Another Move'), + ('confirmed', 'Waiting Availability'), + ('assigned', 'Available'), + ('done', 'Done'), + ], readonly=True, related='out_move_id.state', + string='State of the Outgoing Stock Move') + in_state = fields.Selection([ + ('draft', 'New'), + ('cancel', 'Cancelled'), + ('waiting', 'Waiting Another Move'), + ('confirmed', 'Waiting Availability'), + ('assigned', 'Available'), + ('done', 'Done'), + ], readonly=True, related='in_move_id.state', + string='State of the Return Stock Move') + out_picking_id = fields.Many2one( + 'stock.picking', related='out_move_id.picking_id', + string='Delivery Order', readonly=True) + in_picking_id = fields.Many2one( + 'stock.picking', related='in_move_id.picking_id', + string='Return Picking', readonly=True) + extension_order_line_ids = fields.One2many( + 'sale.order.line', 'extension_rental_id', + string='Rental Extensions', readonly=True) + sell_order_line_ids = fields.One2many( + 'sale.order.line', 'sell_rental_id', + string='Sell Rented Product', readonly=True) + sell_procurement_id = fields.Many2one( + 'procurement.order', string="Sell Procurement", readonly=True, + compute='_compute_procurement_and_move') + sell_move_id = fields.Many2one( + 'stock.move', compute='_compute_procurement_and_move', + string='Sell Stock Move', readonly=True) + sell_state = fields.Selection([ + ('draft', 'New'), + ('cancel', 'Cancelled'), + ('waiting', 'Waiting Another Move'), + ('confirmed', 'Waiting Availability'), + ('assigned', 'Available'), + ('done', 'Done'), + ], readonly=True, related='sell_move_id.state', + string='State of the Sell Stock Move') + sell_picking_id = fields.Many2one( + 'stock.picking', related='sell_move_id.picking_id', + string='Sell Delivery Order', readonly=True) + end_date = fields.Date( + compute='_compute_end_date', string='End Date (extensions included)', + readonly=True, + help="End Date of the Rental, taking into account all the " + "extensions sold to the customer.") + state = fields.Selection([ + ('ordered', 'Ordered'), + ('out', 'Out'), + ('sell_progress', 'Sell in progress'), + ('sold', 'Sold'), + ('in', 'Back In'), + ], string='State', compute='_compute_procurement_and_move', + readonly=True) + + +class StockWarehouse(models.Model): + _inherit = "stock.warehouse" - def name_get(self, cr, uid, ids, context=None): - res = [] - for rental in self.browse(cr, uid, ids, context=context): - res.append(( - rental.id, - u'[%s] %s (%s - %s)' - % ( - rental.partner_id.name, - rental.rented_product_id.name, - rental.start_date, - rental.end_date))) - return res - - def _compute_end_date(self, cr, uid, ids, name, arg, context=None): - res = {} - for rental in self.browse(cr, uid, ids, context=context): - end_date = False - if rental.extension_order_line_ids: - for extension in rental.extension_order_line_ids: - if extension.state not in ('cancel', 'draft'): - if extension.end_date > end_date: - end_date = extension.end_date - if not end_date and rental.start_order_line_id: - end_date = rental.start_order_line_id.end_date - res[rental.id] = end_date - return res - - _columns = { - 'start_order_line_id': fields.many2one( - 'sale.order.line', 'Rental Sale Order Line'), - 'start_date': fields.related( - 'start_order_line_id', 'start_date', - type='date', string='Start Date', readonly=True), - 'rental_product_id': fields.related( - 'start_order_line_id', 'product_id', - type='many2one', relation='product.product', - string="Rental Service", readonly=True), - 'rented_product_id': fields.related( - 'rental_product_id', 'rented_product_id', - type='many2one', relation='product.product', - string="Rented Product", readonly=True), - 'start_order_id': fields.related( - 'start_order_line_id', 'order_id', - type='many2one', relation='sale.order', - string='Rental Sale Order', readonly=True), - 'company_id': fields.related( - 'start_order_id', 'company_id', - type='many2one', relation='res.company', string='Company', - readonly=True), - 'partner_id': fields.related( - 'start_order_id', 'partner_id', - type='many2one', relation='res.partner', string='Partner', - readonly=True), - 'out_move_id': fields.many2one('stock.move', 'Outgoing Stock Move'), - 'in_move_id': fields.related( - 'out_move_id', 'move_dest_id', type='many2one', - relation='stock.move', string='Return Stock Move', readonly=True), - 'out_state': fields.related( - 'out_move_id', 'state', - type='selection', string='State of the Outgoing Stock Move', - selection=[ - ('draft', 'New'), - ('cancel', 'Cancelled'), - ('waiting', 'Waiting Another Move'), - ('confirmed', 'Waiting Availability'), - ('assigned', 'Available'), - ('done', 'Done'), - ], readonly=True), - 'in_state': fields.related( - 'in_move_id', 'state', - type='selection', string='State of the Return Stock Move', - selection=[ - ('draft', 'New'), - ('cancel', 'Cancelled'), - ('waiting', 'Waiting Another Move'), - ('confirmed', 'Waiting Availability'), - ('assigned', 'Available'), - ('done', 'Done'), - ], readonly=True), - 'out_picking_id': fields.related( - 'out_move_id', 'picking_id', - type='many2one', relation='stock.picking', string='Delivery Order', - readonly=True), - 'in_picking_id': fields.related( - 'in_move_id', 'picking_id', - type='many2one', relation='stock.picking', string='Return Picking', - readonly=True), - 'prodlot_id': fields.related( - 'out_move_id', 'prodlot_id', - type='many2one', relation='stock.production.lot', - string='Serial Number', readonly=True), - 'extension_order_line_ids': fields.one2many( - 'sale.order.line', 'extension_rental_id', - 'Rental Extensions', readonly=True), - 'sell_order_line_ids': fields.one2many( - 'sale.order.line', 'sell_rental_id', - 'Sell Rented Product', readonly=True), - 'end_date': fields.function( - _compute_end_date, type='date', - string='End Date (extensions included)', - help="End Date of the Rental, taking into account all the " - "extensions sold to the customer."), - # 'state': # TODO + rental_in_location_id = fields.Many2one( + 'stock.location', 'Rental In', domain=[('usage', '<>', 'view')]) + rental_out_location_id = fields.Many2one( + 'stock.location', 'Rental Out', domain=[('usage', '<>', 'view')]) + rental_allowed = fields.Boolean('Rental Allowed', default=True) + rental_route_id = fields.Many2one( + 'stock.location.route', string='Rental Route') + sell_rented_product_route_id = fields.Many2one( + 'stock.location.route', string='Sell Rented Product Route') + + @api.multi + def _get_rental_push_pull_rules(self): + self.ensure_one() + route_obj = self.env['stock.location.route'] + try: + rental_route = self.env.ref('sale_rental.route_warehouse0_rental') + except: + rental_routes = route_obj.search([('name', '=', _('Rent'))]) + rental_route = rental_routes and rental_routes[0] or False + if not rental_route: + raise Warning(_("Can't find any generic 'Rent' route.")) + try: + sell_rented_product_route = self.env.ref( + 'sale_rental.route_warehouse0_sell_rented_product') + except: + sell_rented_product_routes = route_obj.search( + [('name', '=', _('Sell Rented Product'))]) + sell_rented_product_route =\ + sell_rented_product_routes and sell_rented_product_routes[0]\ + or False + if not sell_rented_product_route: + raise Warning( + _("Can't find any generic 'Sell Rented Product' route.")) + + if not self.rental_in_location_id: + raise Warning( + _("The Rental Input stock location is not set on the " + "warehouse %s") % self.name) + if not self.rental_out_location_id: + raise Warning( + _("The Rental Output stock location is not set on the " + "warehouse %s") % self.name) + rental_pull_rule = { + 'name': self.pool['stock.warehouse']._format_rulename( + self._cr, self._uid, self, self.rental_in_location_id, + self.rental_out_location_id, self.env.context), + 'location_id': self.rental_out_location_id.id, + 'location_src_id': self.rental_in_location_id.id, + 'route_id': rental_route.id, + 'action': 'move', + 'picking_type_id': self.out_type_id.id, + 'warehouse_id': self.id, } + rental_push_rule = { + 'name': self.pool['stock.warehouse']._format_rulename( + self._cr, self._uid, self, self.rental_out_location_id, + self.rental_in_location_id, self.env.context), + 'location_from_id': self.rental_out_location_id.id, + 'location_dest_id': self.rental_in_location_id.id, + 'route_id': rental_route.id, + 'auto': 'auto', + 'invoice_state': 'none', + 'picking_type_id': self.in_type_id.id, + 'warehouse_id': self.id, + } + sell_rented_product_pull_rule = { + 'name': self.pool['stock.warehouse']._format_rulename( + self._cr, self._uid, self, self.rental_out_location_id, + self.out_type_id.default_location_dest_id, self.env.context), + 'location_id': self.out_type_id.default_location_dest_id.id, + 'location_src_id': self.rental_out_location_id.id, + 'route_id': sell_rented_product_route.id, + 'action': 'move', + 'picking_type_id': self.out_type_id.id, + 'warehouse_id': self.id, + } + return { + 'procurement.rule': [ + rental_pull_rule, + sell_rented_product_pull_rule], + 'stock.location.path': [rental_push_rule], + } - -class stock_warehouse(orm.Model): - _inherit = "stock.warehouse" - _columns = { - 'rental_in_location_id': fields.many2one( - 'stock.location', 'Rental Input', - domain=[('usage', '<>', 'view')]), - 'rental_out_location_id': fields.many2one( - 'stock.location', 'Rental Output', - domain=[('usage', '<>', 'view')]), - } + @api.multi + def write(self, vals): + if 'rental_allowed' in vals: + if vals.get('rental_allowed'): + for warehouse in self: + for obj, rules_list in\ + self._get_rental_push_pull_rules().iteritems(): + for rule in rules_list: + self.env[obj].create(rule) + else: + for warehouse in self: + warehouse.rental_route_id.pull_ids.unlink() + warehouse.rental_route_id.push_ids.unlink() + warehouse.sell_rented_product_route_id.pull_ids.unlink() + warehouse.sell_rented_product_route_id.push_ids.unlink() + return super(StockWarehouse, self).write(vals) -class stock_move(orm.Model): +class StockMove(models.Model): _inherit = 'stock.move' - _columns = { - 'sale_rental_ids': fields.one2many( - 'sale.rental', 'out_move_id', 'Rentals', readonly=True), - } - - def action_done(self, cr, uid, ids, context=None): - '''Copy prodlot from outgoing move to incoming move''' - res = super(stock_move, self).action_done( - cr, uid, ids, context=context) - for move in self.browse(cr, uid, ids, context=context): - if ( - move.state == 'done' - and move.move_dest_id - and move.sale_rental_ids - and move.prodlot_id - and not move.move_dest_id.prodlot_id): - self.write( - cr, uid, move.move_dest_id.id, { - 'prodlot_id': move.prodlot_id.id, - }, context=context) - return res + @api.model + def _create_invoice_line_from_vals(self, move, invoice_line_vals): + '''When we invoice from delivery, we shouldn't invoice + the rented product''' + if ( + move.procurement_id + and move.procurement_id.sale_line_id + and move.procurement_id.sale_line_id.rental): + return False + else: + return super(StockMove, self)._create_invoice_line_from_vals( + move, invoice_line_vals) -class stock_picking(orm.Model): - _inherit = 'stock.picking' +class StockLocationPath(models.Model): + _inherit = 'stock.location.path' - # When we invoice from delivery, we shouldn't invoice the rented product - # Beware of this: https://bugs.launchpad.net/openobject-addons/+bug/1167330 - # It will work if your code of the addons/7.0 is after 2013-12-26 - def _invoice_line_hook(self, cr, uid, move_line, invoice_line_id): - res = super(stock_picking, self)._invoice_line_hook( - cr, uid, move_line, invoice_line_id) - if move_line.sale_line_id.rental: - self.pool['account.invoice.line'].unlink(cr, uid, invoice_line_id) - return res + @api.model + def _prepare_push_apply(self, rule, move): + '''Inherit to write the end date of the rental on the return move''' + vals = super(StockLocationPath, self)._prepare_push_apply(rule, move) + if ( + move.procurement_id + and move.procurement_id.location_id == + move.procurement_id.warehouse_id.rental_out_location_id + and move.procurement_id.sale_line_id + and move.procurement_id.sale_line_id + .rental_type == 'new_rental'): + rental_end_date = move.procurement_id.sale_line_id.end_date + vals['date'] = vals['date_expected'] = rental_end_date + return vals diff --git a/sale_rental/rental_data.xml b/sale_rental/rental_data.xml index ae2bb1606d3..f14a476a11d 100644 --- a/sale_rental/rental_data.xml +++ b/sale_rental/rental_data.xml @@ -1,7 +1,7 @@ @@ -12,19 +12,45 @@ Rental In - + Rental Out - + + + + + + + Rent + 100 + + + + + + Sell Rented Product + 100 + + + + + + + 1 + + + diff --git a/sale_rental/rental_demo.xml b/sale_rental/rental_demo.xml index b151b64159f..cd465f952b2 100644 --- a/sale_rental/rental_demo.xml +++ b/sale_rental/rental_demo.xml @@ -1,19 +1,86 @@ + + + + + Rental of one iPad Mini + RENT-A1232 + + + 5 + service + + + + + + + + Rental of one iMac + RENT-A1090 + + + 4 + service + + + + + + + + Rental of one Laptop E5023 + RENT-LAP-E5 + + + 6 + service + + + + + + + + + Inventory for rented products + + + + + + + 56.0 + + - - - + + + + + 46.0 + - - + + + + + 2.0 + + + + diff --git a/sale_rental/rental_view.xml b/sale_rental/rental_view.xml index 19fdf73543a..be25bb3457d 100644 --- a/sale_rental/rental_view.xml +++ b/sale_rental/rental_view.xml @@ -1,7 +1,7 @@ @@ -14,15 +14,20 @@ sale.rental.form sale.rental -
+ +
+ +
+ + @@ -45,7 +50,7 @@ - + @@ -53,7 +58,11 @@ - + + + + +
@@ -70,6 +79,7 @@ + @@ -84,5 +94,6 @@ + diff --git a/sale_rental/sale_view.xml b/sale_rental/sale_view.xml index 5ed9a713eac..a27ef3fae8f 100644 --- a/sale_rental/sale_view.xml +++ b/sale_rental/sale_view.xml @@ -1,7 +1,7 @@ @@ -11,28 +11,38 @@ - rental.view_order_form + sale_rental.view_order_form sale.order - - 100 + - + + attrs="{'invisible': [('rental', '=', False)], 'required': [('rental', '=', True)]}"/> + domain="[('rental_product_id', '=', product_id), ('state', 'in', ('ordered', 'out'))]" /> + domain="[('rented_product_id', '=', product_id), ('state', '=', 'out')]"/> + + + sale_rental.sale_stock.view_order_form + sale.order + + + + product_uom_qty_change_with_wh_with_rental(parent.pricelist_id,product_id,product_uom_qty,product_uom,product_uos_qty,False,name,parent.partner_id, False, False, parent.date_order, product_packaging, parent.fiscal_position, True, parent.warehouse_id, rental_type, rental_qty, context) + + + diff --git a/sale_rental/stock_view.xml b/sale_rental/stock_view.xml index 67f874ea296..d89c4f000c8 100644 --- a/sale_rental/stock_view.xml +++ b/sale_rental/stock_view.xml @@ -1,7 +1,7 @@ @@ -11,13 +11,18 @@ - rental.warehouse.form + rental.stock.warehouse.form stock.warehouse - - - + + + + + + + + diff --git a/sale_rental/wizard/__init__.py b/sale_rental/wizard/__init__.py index 75116f62db3..873100cd027 100644 --- a/sale_rental/wizard/__init__.py +++ b/sale_rental/wizard/__init__.py @@ -1,8 +1,8 @@ # -*- encoding: utf-8 -*- ############################################################################## # -# Sale Rental module for OpenERP -# Copyright (C) 2014 Akretion (http://www.akretion.com) +# Sale Rental module for Odoo +# Copyright (C) 2014-2015 Akretion (http://www.akretion.com) # @author Alexis de Lattre # # This program is free software: you can redistribute it and/or modify diff --git a/sale_rental/wizard/create_rental_product.py b/sale_rental/wizard/create_rental_product.py index 2c7c3ca66cf..e67ace09136 100644 --- a/sale_rental/wizard/create_rental_product.py +++ b/sale_rental/wizard/create_rental_product.py @@ -1,8 +1,8 @@ # -*- encoding: utf-8 -*- ############################################################################## # -# Sale Rental module for OpenERP -# Copyright (C) 2014 Akretion (http://www.akretion.com) +# Sale Rental module for Odoo +# Copyright (C) 2014-2015 Akretion (http://www.akretion.com) # @author Alexis de Lattre # # This program is free software: you can redistribute it and/or modify @@ -20,93 +20,74 @@ # ############################################################################## -from openerp.osv import orm, fields +from openerp import models, fields, api, _ import openerp.addons.decimal_precision as dp -from openerp.tools.translate import _ -class create_rental_product(orm.TransientModel): +class CreateRentalProduct(models.TransientModel): _name = 'create.rental.product' _description = 'Create the Rental Service Product' - _columns = { - 'sale_price_per_day': fields.float( - 'Sale Price per Day', required=True, - digits_compute=dp.get_precision('Product Price')), - # I would like to translate the field 'name_prefix', but - # it doesn't seem to work in a wizard - 'name': fields.char( - 'Product Name', size=64, required=True), - 'default_code': fields.char( - 'Default Code', size=16, required=True), - 'categ_id': fields.many2one( - 'product.category', 'Product Category', required=True), - } + @api.model + def _default_name(self): + assert self.env.context.get('active_model') == 'product.product',\ + 'Wrong underlying model, should be product.product' + hw_product = self.env['product.product'].browse( + self.env.context['active_id']) + return _('Rental of a %s') % hw_product.name - def default_get(self, cr, uid, fields_list, context=None): - res = super(create_rental_product, self).default_get( - cr, uid, fields_list, context=context) - if not res: - res = {} - if context is None: - context = {} - assert context.get('active_model') == 'product.product',\ + @api.model + def _default_code(self): + assert self.env.context.get('active_model') == 'product.product',\ 'Wrong underlying model, should be product.product' - hw_product = self.pool['product.product'].browse( - cr, uid, context['active_id'], context=context) - res.update({ - 'sale_price_per_day': 1.0, - 'default_code': _('RENT-%s') % hw_product.default_code, - 'name': _('Rental of one %s') % hw_product.name, - }) - return res + hw_product = self.env['product.product'].browse( + self.env.context['active_id']) + return _('RENT-%s') % hw_product.default_code + + sale_price_per_day = fields.Float( + string='Rental Price per Day', required=True, + digits=dp.get_precision('Product Price'), default=1.0) + name = fields.Char( + string='Product Name', size=64, required=True, + default=_default_name) + default_code = fields.Char( + string='Default Code', size=16, required=True, + default=_default_code) + categ_id = fields.Many2one( + 'product.category', string='Product Category', required=True) - def create_rental_product(self, cr, uid, ids, context=None): - assert len(ids) == 1, 'Only 1 ID' - if context is None: - context = {} + @api.multi + def create_rental_product(self): + self.ensure_one() # check that a rental product doesn't already exists ? - hw_product_id = context.get('active_id') - pp_obj = self.pool['product.product'] - hw_product = pp_obj.browse(cr, uid, hw_product_id, context=context) - assert isinstance(hw_product_id, int), 'Active ID is not set' - wiz = self.browse(cr, uid, ids[0], context=context) - (uom_model, day_uom_id) = self.pool['ir.model.data'].\ - get_object_reference(cr, uid, 'product', 'product_uom_day') - assert uom_model == 'product.uom', 'Must be product.uom' + assert self.env.context.get('active_model') == 'product.product',\ + 'Wrong underlying model, should be product.product' + hw_product_id = self.env.context.get('active_id') + assert hw_product_id, 'Active ID is not set' + pp_obj = self.env['product.product'] + day_uom_id = self.env.ref('product.product_uom_day').id - product_id = pp_obj.create(cr, uid, { + product = pp_obj.create({ 'type': 'service', 'sale_ok': True, 'purchase_ok': False, 'uom_id': day_uom_id, 'uom_po_id': day_uom_id, - 'list_price': wiz.sale_price_per_day, - 'name': wiz.name, - 'default_code': wiz.default_code, + 'list_price': self.sale_price_per_day, + 'name': self.name, + 'default_code': self.default_code, 'rented_product_id': hw_product_id, 'must_have_dates': True, - 'categ_id': wiz.categ_id.id, - }, context=context) + 'categ_id': self.categ_id.id, + }) -# lang_ids = self.pool['res.lang'].search(cr, uid, [], context=context) -# for lang in self.pool['res.lang'].browse( -# cr, uid, lang_ids, context=context): -# ctx_lang = context.copy() -# ctx_lang['lang'] = ctx_lang.code -# wiz_lang = self.browse(cr, uid, ids[0], context=ctx_lang) -# pp_obj.write(cr, uid, product_id, { -# 'name': '%s%s' % ( -# wiz.name_prefix and wiz.name_prefix + ' ' or '', -# hw_product.name), -# }, context=ctx_lang) - return { + action = { 'name': pp_obj._description, 'type': 'ir.actions.act_window', 'res_model': pp_obj._name, - 'view_type': 'form', 'view_mode': 'form,tree,kanban', 'nodestroy': False, # Close the wizard pop-up 'target': 'current', - 'res_id': product_id, + 'res_id': product.id, } + return action diff --git a/sale_rental/wizard/create_rental_product_view.xml b/sale_rental/wizard/create_rental_product_view.xml index 56637890151..169581c4919 100644 --- a/sale_rental/wizard/create_rental_product_view.xml +++ b/sale_rental/wizard/create_rental_product_view.xml @@ -1,7 +1,7 @@ @@ -13,7 +13,7 @@ rental.product.product.form create.rental.product -
+ @@ -32,7 +32,6 @@ Create Rental Service create.rental.product - form form new diff --git a/sale_start_end_dates/README.rst b/sale_start_end_dates/README.rst new file mode 100644 index 00000000000..021a7cfdd6f --- /dev/null +++ b/sale_start_end_dates/README.rst @@ -0,0 +1,29 @@ +Sale Start End Dates +==================== + +This module adds the fields *start date* and *end date* on sale order +lines. Upon invoice creation, the value of the start/end dates of the +sale order line is copied to the start/end dates of invoice lines. This +module is a technical module: it is designed to be used with other +modules such as the *sale_rental* module. + +Credits +======= + +Contributors +------------ + +* Alexis de Lattre + +Maintainer +---------- + +.. image:: http://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: http://odoo-community.org + +This module is maintained by the OCA. + +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. + +To contribute to this module, please visit http://odoo-community.org. diff --git a/sale_start_end_dates/__init__.py b/sale_start_end_dates/__init__.py index e3fa3783da1..5dad96f3cbd 100644 --- a/sale_start_end_dates/__init__.py +++ b/sale_start_end_dates/__init__.py @@ -1,8 +1,8 @@ # -*- encoding: utf-8 -*- ############################################################################## # -# Sale Start End Dates module for OpenERP -# Copyright (C) 2014 Akretion (http://www.akretion.com) +# Sale Start End Dates module for Odoo +# Copyright (C) 2014-2015 Akretion (http://www.akretion.com) # @author Alexis de Lattre # # This program is free software: you can redistribute it and/or modify diff --git a/sale_start_end_dates/__openerp__.py b/sale_start_end_dates/__openerp__.py index 70391f553e7..50dadad0505 100644 --- a/sale_start_end_dates/__openerp__.py +++ b/sale_start_end_dates/__openerp__.py @@ -1,8 +1,8 @@ # -*- encoding: utf-8 -*- ############################################################################## # -# Sale Start End Dates module for OpenERP -# Copyright (C) 2014 Akretion (http://www.akretion.com) +# Sale Start End Dates module for Odoo +# Copyright (C) 2014-2015 Akretion (http://www.akretion.com) # @author Alexis de Lattre # # This program is free software: you can redistribute it and/or modify @@ -27,20 +27,10 @@ 'category': 'Sales Management', 'license': 'AGPL-3', 'summary': 'Adds start date and end date on sale order lines', - 'description': """ -Sale Start End Dates -==================== - -This module adds start date and end dates on sale order lines. The value of these field is copied to the start date and end date of invoice lines. This module is designed to be used with other modules such as sale_rental. - -Please contact Alexis de Lattre from Akretion for any help or question about this module. - """, - 'author': 'Akretion', + 'author': 'Akretion,Odoo Community Association (OCA)', 'website': 'http://www.akretion.com', - 'depends': ['account_cutoff_prepaid', 'sale'], - 'data': [ - 'sale_view.xml', - ], + 'depends': ['account_cutoff_prepaid', 'sale', 'web_context_tunnel'], + 'data': ['sale_view.xml'], + 'demo': ['sale_demo.xml'], 'installable': True, - 'active': False, } diff --git a/sale_start_end_dates/i18n/sale_start_end_dates.pot b/sale_start_end_dates/i18n/sale_start_end_dates.pot index 9e9ebefdeee..cef3c4412dc 100644 --- a/sale_start_end_dates/i18n/sale_start_end_dates.pot +++ b/sale_start_end_dates/i18n/sale_start_end_dates.pot @@ -1,13 +1,13 @@ -# Translation of OpenERP Server. +# Translation of Odoo Server. # This file contains the translation of the following modules: # * sale_start_end_dates # msgid "" msgstr "" -"Project-Id-Version: OpenERP Server 7.0\n" +"Project-Id-Version: Odoo Server 8.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2014-02-02 22:30+0000\n" -"PO-Revision-Date: 2014-02-02 22:30+0000\n" +"POT-Creation-Date: 2015-02-26 11:07+0000\n" +"PO-Revision-Date: 2015-02-26 11:07+0000\n" "Last-Translator: <>\n" "Language-Team: \n" "MIME-Version: 1.0\n" @@ -16,33 +16,36 @@ msgstr "" "Plural-Forms: \n" #. module: sale_start_end_dates -#: field:sale.order.line,end_date:0 -msgid "End Date" +#: field:sale.order,default_end_date:0 +msgid "Default End Date" msgstr "" #. module: sale_start_end_dates -#: code:addons/sale_start_end_dates/sale.py:94 -#, python-format -msgid "Start Date should be before or be the same as End Date." +#: field:sale.order,default_start_date:0 +msgid "Default Start Date" msgstr "" #. module: sale_start_end_dates -#: code:addons/sale_start_end_dates/sale.py:47 +#: code:addons/sale_start_end_dates/sale.py:40 #, python-format -msgid "Missing End Date for sale order line with Description '%s'." +msgid "Default Start Date should be before or be the same as Default End Date for sale order %s" +msgstr "" + +#. module: sale_start_end_dates +#: field:sale.order.line,end_date:0 +msgid "End Date" msgstr "" #. module: sale_start_end_dates -#: constraint:sale.order.line:0 -msgid "Error msg in raise" +#: code:addons/sale_start_end_dates/sale.py:89 +#, python-format +msgid "Missing End Date for sale order line with Product '%s'." msgstr "" #. module: sale_start_end_dates -#: code:addons/sale_start_end_dates/sale.py:46 -#: code:addons/sale_start_end_dates/sale.py:52 -#: code:addons/sale_start_end_dates/sale.py:59 +#: code:addons/sale_start_end_dates/sale.py:94 #, python-format -msgid "Error:" +msgid "Missing Start Date for sale order line with Product '%s'." msgstr "" #. module: sale_start_end_dates @@ -51,15 +54,13 @@ msgid "Must Have Start and End Dates" msgstr "" #. module: sale_start_end_dates -#: code:addons/sale_start_end_dates/sale.py:93 -#, python-format -msgid "Warning:" +#: model:ir.model,name:sale_start_end_dates.model_sale_order +msgid "Sales Order" msgstr "" #. module: sale_start_end_dates -#: code:addons/sale_start_end_dates/sale.py:53 -#, python-format -msgid "Missing Start Date for sale order line with Description '%s'." +#: model:ir.model,name:sale_start_end_dates.model_sale_order_line +msgid "Sales Order Line" msgstr "" #. module: sale_start_end_dates @@ -68,13 +69,18 @@ msgid "Start Date" msgstr "" #. module: sale_start_end_dates -#: code:addons/sale_start_end_dates/sale.py:60 +#: code:addons/sale_start_end_dates/sale.py:99 #, python-format -msgid "Start Date should be before or be the same as End Date for sale order line with Description '%s'." +msgid "Start Date should be before or be the same as End Date for sale order line with Product '%s'." msgstr "" #. module: sale_start_end_dates -#: model:ir.model,name:sale_start_end_dates.model_sale_order_line -msgid "Sales Order Line" +#: view:sale.order:sale_start_end_dates.view_order_form +msgid "{'default_end_date': parent.default_end_date}" +msgstr "" + +#. module: sale_start_end_dates +#: view:sale.order:sale_start_end_dates.view_order_form +msgid "{'default_start_date': parent.default_start_date}" msgstr "" diff --git a/sale_start_end_dates/sale.py b/sale_start_end_dates/sale.py index 77e01396f72..9a66e120fcf 100644 --- a/sale_start_end_dates/sale.py +++ b/sale_start_end_dates/sale.py @@ -1,8 +1,8 @@ # -*- encoding: utf-8 -*- ############################################################################## # -# Sale Start End Dates module for OpenERP -# Copyright (C) 2014 Akretion (http://www.akretion.com) +# Sale Start End Dates module for Odoo +# Copyright (C) 2014-2015 Akretion (http://www.akretion.com) # @author Alexis de Lattre # # This program is free software: you can redistribute it and/or modify @@ -20,59 +20,97 @@ # ############################################################################## -from openerp.osv import orm, fields -from openerp.tools.translate import _ +from openerp import models, fields, api, _ +from openerp.exceptions import ValidationError -class sale_order_line(orm.Model): +class SaleOrder(models.Model): + _inherit = 'sale.order' + + default_start_date = fields.Date(string='Default Start Date') + default_end_date = fields.Date(string='Default End Date') + + @api.one + @api.constrains('default_start_date', 'default_end_date') + def _check_default_start_end_dates(self): + if ( + self.default_start_date and + self.default_end_date and + self.default_start_date > self.default_end_date): + raise ValidationError( + _("Default Start Date should be before or be the " + "same as Default End Date for sale order %s") + % self.name) + + @api.onchange('default_start_date') + def default_start_date_change(self): + if ( + self.default_start_date and + self.default_end_date and + self.default_start_date > self.default_end_date): + self.default_end_date = self.default_start_date + + @api.onchange('default_end_date') + def default_end_date_change(self): + if ( + self.default_start_date and + self.default_end_date and + self.default_start_date > self.default_end_date): + self.default_start_date = self.default_end_date + + +class SaleOrderLine(models.Model): _inherit = 'sale.order.line' - _columns = { - 'start_date': fields.date( - 'Start Date', - readonly=True, states={'draft': [('readonly', False)]}), - 'end_date': fields.date( - 'End Date', - readonly=True, states={'draft': [('readonly', False)]}), - 'must_have_dates': fields.boolean( - 'Must Have Start and End Dates', - readonly=True, states={'draft': [('readonly', False)]}), - } - - def _check_start_end_dates(self, cr, uid, ids): - for line in self.browse(cr, uid, ids): - if line.start_date and not line.end_date: - raise orm.except_orm( - _('Error:'), + @api.one + @api.depends('start_date', 'end_date') + def _compute_number_of_days(self): + if self.start_date and self.end_date: + self.number_of_days = ( + fields.Date.from_string(self.end_date) + - fields.Date.from_string(self.start_date)).days + 1 + else: + self.number_of_days = 0 + + start_date = fields.Date( + string='Start Date', readonly=True, + states={'draft': [('readonly', False)]}) + end_date = fields.Date( + string='End Date', readonly=True, + states={'draft': [('readonly', False)]}) + number_of_days = fields.Integer( + compute='_compute_number_of_days', string='Number of Days', + readonly=True) + must_have_dates = fields.Boolean( + string='Must Have Start and End Dates', readonly=True, + states={'draft': [('readonly', False)]}) + + @api.one + @api.constrains('start_date', 'end_date') + def _check_start_end_dates(self): + if self.product_id and self.must_have_dates: + if not self.end_date: + raise ValidationError( _("Missing End Date for sale order line with " - "Description '%s'.") - % (line.name)) - if line.end_date and not line.start_date: - raise orm.except_orm( - _('Error:'), + "Product '%s'.") + % (self.product_id.name)) + if not self.start_date: + raise ValidationError( _("Missing Start Date for sale order line with " - "Description '%s'.") - % (line.name)) - if line.end_date and line.start_date and \ - line.start_date > line.end_date: - raise orm.except_orm( - _('Error:'), + "Product '%s'.") + % (self.product_id.name)) + if self.start_date > self.end_date: + raise ValidationError( _("Start Date should be before or be the same as " - "End Date for sale order line with Description '%s'.") - % (line.name)) - return True + "End Date for sale order line with Product '%s'.") + % (self.product_id.name)) # TODO check must_have_dates on SO validation ? or in constraint ? - _constraints = [ - (_check_start_end_dates, "Error msg in raise", - ['start_date', 'end_date', 'product_id']), - ] - - def _prepare_order_line_invoice_line( - self, cr, uid, line, account_id=False, context=None): - res = super(sale_order_line, self)._prepare_order_line_invoice_line( - cr, uid, line, account_id=account_id, context=context) + @api.model + def _prepare_order_line_invoice_line(self, line, account_id=False): + res = super(SaleOrderLine, self)._prepare_order_line_invoice_line( + line, account_id=account_id) if line.must_have_dates: res.update({ 'start_date': line.start_date, @@ -80,33 +118,49 @@ def _prepare_order_line_invoice_line( }) return res - def start_end_dates_change( - self, cr, uid, ids, start_date, end_date, product_id, - product_uom_qty, context=None): - '''This function is designed to be inherited''' - res = {} - if start_date and end_date: - if end_date < start_date: - # We could have put a raise here - # but a warning is fine because we have the constraint - res['warning'] = { - 'title': _('Warning:'), - 'message': _("Start Date should be before or be the " - "same as End Date."), - } - return res +# Disable this method, because I can't put both an on_change in the +# XML and an @api.onchange on the same fields :-( +# And I prefer to use @api.onchange on higher-level modules +# @api.multi +# def start_end_dates_change(self, start_date, end_date): +# res = {} +# if start_date and end_date: +# if end_date < start_date: +# # We could have put a raise here +# # but a warning is fine because we have the constraint +# res['warning'] = { +# 'title': _('Warning:'), +# 'message': _("Start Date should be before or be the " +# "same as End Date."), +# } +# return res + + @api.onchange('end_date') + def end_date_change(self): + if ( + self.start_date and self.end_date and + self.start_date > self.end_date): + self.start_date = self.end_date + + @api.onchange('start_date') + def start_date_change(self): + if ( + self.start_date and self.end_date and + self.start_date > self.end_date): + self.end_date = self.start_date + @api.multi def product_id_change( - self, cr, uid, ids, pricelist, product, qty=0, + self, pricelist, product, qty=0, uom=False, qty_uos=0, uos=False, name='', partner_id=False, lang=False, update_tax=True, date_order=False, packaging=False, - fiscal_position=False, flag=False, context=None): - res = super(sale_order_line, self).product_id_change( - cr, uid, ids, pricelist, product, qty=qty, uom=uom, + fiscal_position=False, flag=False): + res = super(SaleOrderLine, self).product_id_change( + pricelist, product, qty=qty, uom=uom, qty_uos=qty_uos, uos=uos, name=name, partner_id=partner_id, lang=lang, update_tax=update_tax, date_order=date_order, packaging=packaging, fiscal_position=fiscal_position, - flag=flag, context=context) + flag=flag) if not product: res['value'].update({ 'must_have_dates': False, @@ -114,10 +168,15 @@ def product_id_change( 'end_date': False, }) else: - product_o = self.pool['product.product'].browse( - cr, uid, product, context=context) + product_o = self.env['product.product'].browse(product) if product_o.must_have_dates: - res['value'].update({'must_have_dates': True}) + res['value']['must_have_dates'] = True + if self.env.context.get('default_start_date'): + res['value']['start_date'] = self.env.context.get( + 'default_start_date') + if self.env.context.get('default_end_date'): + res['value']['end_date'] = self.env.context.get( + 'default_end_date') else: res['value'].update({ 'must_have_dates': False, diff --git a/sale_start_end_dates/sale_demo.xml b/sale_start_end_dates/sale_demo.xml new file mode 100644 index 00000000000..b8a064a43c4 --- /dev/null +++ b/sale_start_end_dates/sale_demo.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + diff --git a/sale_start_end_dates/sale_view.xml b/sale_start_end_dates/sale_view.xml index 16d18e6b6af..cc11308d746 100644 --- a/sale_start_end_dates/sale_view.xml +++ b/sale_start_end_dates/sale_view.xml @@ -1,7 +1,7 @@ @@ -15,15 +15,37 @@ sale.order - + + + + + + + + + + + + + + {'default_start_date': parent.default_start_date} + {'default_end_date': parent.default_end_date} + + + {'default_start_date': parent.default_start_date} + {'default_end_date': parent.default_end_date} + + + {'default_start_date': parent.default_start_date} + {'default_end_date': parent.default_end_date} +