diff --git a/sale_order_rental/README.rst b/sale_order_rental/README.rst new file mode 100644 index 00000000..e47289c8 --- /dev/null +++ b/sale_order_rental/README.rst @@ -0,0 +1,28 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +================= +Sale order rental +================= + +* This module put the start date, end date and rental days in sale orders, delivery notes and invoices. + + +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 smash it by providing detailed and welcomed feedback. + +Credits +======= + +Contributors +------------ +* Ana Juaristi +* Oihana Larrañaga + +Do not contact contributors directly about support or help with technical issues. diff --git a/sale_order_rental/__init__.py b/sale_order_rental/__init__.py new file mode 100644 index 00000000..aee8895e --- /dev/null +++ b/sale_order_rental/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizards diff --git a/sale_order_rental/__manifest__.py b/sale_order_rental/__manifest__.py new file mode 100644 index 00000000..1bf63436 --- /dev/null +++ b/sale_order_rental/__manifest__.py @@ -0,0 +1,21 @@ +# Copyright 2019 Oihana Larrañaga - AvanzOSC +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +{ + "name": "Sale Order Rental", + "version": "12.0.1.0.0", + "license": "AGPL-3", + "depends": [ + "sale_management", + "stock", + ], + "author": "AvanzOSC", + "website": "http://www.avanzosc.es", + "category": "Sales", + "data": [ + "security/ir.model.access.csv", + "views/sale_order_view.xml", + "views/stock_picking_view.xml", + "views/account_invoice_view.xml", + ], + "installable": True, +} diff --git a/sale_order_rental/i18n/es.po b/sale_order_rental/i18n/es.po new file mode 100644 index 00000000..d57371c8 --- /dev/null +++ b/sale_order_rental/i18n/es.po @@ -0,0 +1,98 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * sale_order_rental +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-07-30 07:20+0000\n" +"PO-Revision-Date: 2019-07-30 07:20+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: sale_order_rental +#: model:ir.model.fields,field_description:sale_order_rental.field_account_invoice__expected_delivery_date +#: model:ir.model.fields,field_description:sale_order_rental.field_account_invoice_line__expected_delivery_date +#: model:ir.model.fields,field_description:sale_order_rental.field_sale_order__expected_delivery_date +#: model:ir.model.fields,field_description:sale_order_rental.field_sale_order_line__expected_delivery_date +#: model:ir.model.fields,field_description:sale_order_rental.field_stock_move__expected_delivery_date +#: model:ir.model.fields,field_description:sale_order_rental.field_stock_picking__expected_delivery_date +msgid "Expected delivery date" +msgstr "Fecha inicio" + +#. module: sale_order_rental +#: model:ir.model.fields,field_description:sale_order_rental.field_account_invoice__expected_end_date +#: model:ir.model.fields,field_description:sale_order_rental.field_account_invoice_line__expected_end_date +#: model:ir.model.fields,field_description:sale_order_rental.field_sale_order__expected_end_date +#: model:ir.model.fields,field_description:sale_order_rental.field_sale_order_line__expected_end_date +#: model:ir.model.fields,field_description:sale_order_rental.field_stock_move__expected_end_date +#: model:ir.model.fields,field_description:sale_order_rental.field_stock_picking__expected_end_date +msgid "Expected end date" +msgstr "Fecha fin" + +#. module: sale_order_rental +#: code:addons/sale_order_rental/models/sale_order.py:37 +#: code:addons/sale_order_rental/models/sale_order.py:96 +#, python-format +msgid "Expected end date must be greater than expected delivery date" +msgstr "La fecha fin tiene que ser mayor que la fecha inicio" + +#. module: sale_order_rental +#: model:ir.model,name:sale_order_rental.model_account_invoice +msgid "Invoice" +msgstr "Factura" + +#. module: sale_order_rental +#: model:ir.model,name:sale_order_rental.model_account_invoice_line +msgid "Invoice Line" +msgstr "Linea de Factura" + +#. module: sale_order_rental +#: model:ir.model.fields,field_description:sale_order_rental.field_account_invoice_line__rental_days +#: model:ir.model.fields,field_description:sale_order_rental.field_sale_order__rental_days +#: model:ir.model.fields,field_description:sale_order_rental.field_sale_order_line__rental_days +#: model:ir.model.fields,field_description:sale_order_rental.field_stock_move__rental_days +#: model:ir.model.fields,field_description:sale_order_rental.field_stock_picking__rental_days +msgid "Rental days" +msgstr "Días de alquiler" + +#. module: sale_order_rental +#: model:ir.model,name:sale_order_rental.model_stock_return_picking +msgid "Return Picking" +msgstr "Albarán de devolución" + +#. module: sale_order_rental +#: model:ir.model,name:sale_order_rental.model_sale_order +msgid "Sale Order" +msgstr "Pedido de venta" + +#. module: sale_order_rental +#: model:ir.model.fields,field_description:sale_order_rental.field_account_invoice__sale_order_id +msgid "Sale order" +msgstr "Pedido" + +#. module: sale_order_rental +#: model:ir.model,name:sale_order_rental.model_sale_order_line +msgid "Sales Order Line" +msgstr "Línea de pedido de venta" + +#. module: sale_order_rental +#: model:ir.model,name:sale_order_rental.model_stock_move +msgid "Stock Move" +msgstr "Movimiento de existencias" + +#. module: sale_order_rental +#: model:ir.model,name:sale_order_rental.model_stock_picking +msgid "Transfer" +msgstr "Transferir" + +#. module: sale_order_rental +#: model:ir.model.fields,field_description:sale_order_rental.field_account_invoice__rental_days +msgid "rental days" +msgstr "Días de alquiler" + diff --git a/sale_order_rental/i18n/sale_order_rental.pot b/sale_order_rental/i18n/sale_order_rental.pot new file mode 100644 index 00000000..0917cfc2 --- /dev/null +++ b/sale_order_rental/i18n/sale_order_rental.pot @@ -0,0 +1,98 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * sale_order_rental +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-07-30 07:20+0000\n" +"PO-Revision-Date: 2019-07-30 07:20+0000\n" +"Last-Translator: <>\n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: sale_order_rental +#: model:ir.model.fields,field_description:sale_order_rental.field_account_invoice__expected_delivery_date +#: model:ir.model.fields,field_description:sale_order_rental.field_account_invoice_line__expected_delivery_date +#: model:ir.model.fields,field_description:sale_order_rental.field_sale_order__expected_delivery_date +#: model:ir.model.fields,field_description:sale_order_rental.field_sale_order_line__expected_delivery_date +#: model:ir.model.fields,field_description:sale_order_rental.field_stock_move__expected_delivery_date +#: model:ir.model.fields,field_description:sale_order_rental.field_stock_picking__expected_delivery_date +msgid "Expected delivery date" +msgstr "" + +#. module: sale_order_rental +#: model:ir.model.fields,field_description:sale_order_rental.field_account_invoice__expected_end_date +#: model:ir.model.fields,field_description:sale_order_rental.field_account_invoice_line__expected_end_date +#: model:ir.model.fields,field_description:sale_order_rental.field_sale_order__expected_end_date +#: model:ir.model.fields,field_description:sale_order_rental.field_sale_order_line__expected_end_date +#: model:ir.model.fields,field_description:sale_order_rental.field_stock_move__expected_end_date +#: model:ir.model.fields,field_description:sale_order_rental.field_stock_picking__expected_end_date +msgid "Expected end date" +msgstr "" + +#. module: sale_order_rental +#: code:addons/sale_order_rental/models/sale_order.py:37 +#: code:addons/sale_order_rental/models/sale_order.py:96 +#, python-format +msgid "Expected end date must be greater than expected delivery date" +msgstr "" + +#. module: sale_order_rental +#: model:ir.model,name:sale_order_rental.model_account_invoice +msgid "Invoice" +msgstr "" + +#. module: sale_order_rental +#: model:ir.model,name:sale_order_rental.model_account_invoice_line +msgid "Invoice Line" +msgstr "" + +#. module: sale_order_rental +#: model:ir.model.fields,field_description:sale_order_rental.field_account_invoice_line__rental_days +#: model:ir.model.fields,field_description:sale_order_rental.field_sale_order__rental_days +#: model:ir.model.fields,field_description:sale_order_rental.field_sale_order_line__rental_days +#: model:ir.model.fields,field_description:sale_order_rental.field_stock_move__rental_days +#: model:ir.model.fields,field_description:sale_order_rental.field_stock_picking__rental_days +msgid "Rental days" +msgstr "" + +#. module: sale_order_rental +#: model:ir.model,name:sale_order_rental.model_stock_return_picking +msgid "Return Picking" +msgstr "" + +#. module: sale_order_rental +#: model:ir.model,name:sale_order_rental.model_sale_order +msgid "Sale Order" +msgstr "" + +#. module: sale_order_rental +#: model:ir.model.fields,field_description:sale_order_rental.field_account_invoice__sale_order_id +msgid "Sale order" +msgstr "" + +#. module: sale_order_rental +#: model:ir.model,name:sale_order_rental.model_sale_order_line +msgid "Sales Order Line" +msgstr "" + +#. module: sale_order_rental +#: model:ir.model,name:sale_order_rental.model_stock_move +msgid "Stock Move" +msgstr "" + +#. module: sale_order_rental +#: model:ir.model,name:sale_order_rental.model_stock_picking +msgid "Transfer" +msgstr "" + +#. module: sale_order_rental +#: model:ir.model.fields,field_description:sale_order_rental.field_account_invoice__rental_days +msgid "rental days" +msgstr "" + diff --git a/sale_order_rental/models/__init__.py b/sale_order_rental/models/__init__.py new file mode 100644 index 00000000..90ffdafa --- /dev/null +++ b/sale_order_rental/models/__init__.py @@ -0,0 +1,6 @@ +from . import sale_order +from . import sale_order_line +from . import stock_picking +from . import stock_move +from . import account_invoice +from . import account_invoice_line diff --git a/sale_order_rental/models/account_invoice.py b/sale_order_rental/models/account_invoice.py new file mode 100644 index 00000000..78b24b2d --- /dev/null +++ b/sale_order_rental/models/account_invoice.py @@ -0,0 +1,65 @@ +# Copyright 2019 Oihana Larrañaga - AvanzOSC +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +from odoo import api, fields, models + + +class AccountInvoice(models.Model): + _inherit = 'account.invoice' + + sale_order_id = fields.Many2one( + string='Sale order', comodel_name='sale.order') + sale_expected_delivery_date = fields.Date( + string='Expected Delivery Date', + compute='_compute_rental_dates', store=True) + sale_expected_end_date = fields.Date( + string='Expected Return Date', + compute='_compute_rental_dates', store=True) + rental_days = fields.Integer( + string='Rental Days', + compute='_compute_rental_dates', store=True) + + @api.depends('invoice_line_ids', + 'invoice_line_ids.sale_expected_delivery_date', + 'invoice_line_ids.sale_expected_end_date') + def _compute_rental_dates(self): + for invoice in self.filtered('invoice_line_ids'): + lines = invoice.invoice_line_ids.filtered( + lambda l: l.sale_expected_delivery_date or + l.sale_expected_end_date) + if lines: + invoice.sale_expected_delivery_date = min(lines.mapped( + 'sale_expected_delivery_date')) + invoice.sale_expected_end_date = max(lines.mapped( + 'sale_expected_end_date')) + invoice.rental_days = (( + invoice.sale_expected_end_date - + invoice.sale_expected_delivery_date).days + 1) + + @api.multi + def get_taxes_values(self): + tax_grouped = super(AccountInvoice, self).get_taxes_values() + if any(self.invoice_line_ids.filtered('rental_days')): + tax_grouped = {} + round_curr = self.currency_id.round + for line in self.invoice_line_ids: + if not line.account_id: + continue + price_unit = line.price_unit * ( + 1 - (line.discount or 0.0) / 100.0) + quantity = line.quantity + if line.rental_days: + quantity *= line.rental_days + taxes = line.invoice_line_tax_ids.compute_all( + price_unit, self.currency_id, quantity, line.product_id, + self.partner_id)['taxes'] + for tax in taxes: + val = self._prepare_tax_line_vals(line, tax) + key = self.env['account.tax'].browse( + tax['id']).get_grouping_key(val) + if key not in tax_grouped: + tax_grouped[key] = val + tax_grouped[key]['base'] = round_curr(val['base']) + else: + tax_grouped[key]['amount'] += val['amount'] + tax_grouped[key]['base'] += round_curr(val['base']) + return tax_grouped diff --git a/sale_order_rental/models/account_invoice_line.py b/sale_order_rental/models/account_invoice_line.py new file mode 100644 index 00000000..415a67d1 --- /dev/null +++ b/sale_order_rental/models/account_invoice_line.py @@ -0,0 +1,69 @@ +# Copyright 2019 Oihana Larrañaga - AvanzOSC +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +from odoo import api, fields, models + + +class AccountInvoiceLine(models.Model): + _inherit = 'account.invoice.line' + + sale_expected_delivery_date = fields.Date( + string='Expected Delivery Date', + compute='_compute_sale_rental_dates', store=True) + sale_expected_end_date = fields.Date( + string='Expected Return Date', + compute='_compute_sale_rental_dates', store=True) + rental_days = fields.Integer( + string='Rental Days', + compute='_compute_sale_rental_dates', store=True) + + @api.depends('sale_line_ids', + 'sale_line_ids.expected_delivery_date', + 'sale_line_ids.expected_end_date', + 'sale_line_ids.rental_days') + def _compute_sale_rental_dates(self): + for line in self.filtered('sale_line_ids'): + sale_lines = line.sale_line_ids.filtered( + lambda l: l.expected_delivery_date and + l.expected_end_date) + if sale_lines: + line.sale_expected_delivery_date = min(sale_lines.mapped( + 'expected_delivery_date')) + line.sale_expected_end_date = max(sale_lines.mapped( + 'expected_end_date')) + line.rental_days = sum(sale_lines.mapped('rental_days')) + + @api.depends('price_unit', 'discount', 'invoice_line_tax_ids', 'quantity', + 'rental_days', 'product_id', 'invoice_id.partner_id', + 'invoice_id.currency_id', 'invoice_id.company_id', + 'invoice_id.date_invoice', 'invoice_id.date') + def _compute_price(self): + for line in self: + if line.rental_days: + currency = ( + line.invoice_id and line.invoice_id.currency_id or None) + price = line.price_unit * (1 - (line.discount or 0.0) / 100.0) + taxes = False + quantity = line.quantity * line.rental_days + if line.invoice_line_tax_ids: + taxes = line.invoice_line_tax_ids.compute_all( + price, currency, quantity, product=line.product_id, + partner=line.invoice_id.partner_id) + line.price_subtotal = price_subtotal_signed = taxes[ + 'total_excluded'] if taxes else quantity * price + line.price_total = taxes[ + 'total_included'] if taxes else line.price_subtotal + if (line.invoice_id.currency_id and + line.invoice_id.currency_id != + line.invoice_id.company_id.currency_id): + currency = line.invoice_id.currency_id + date = line.invoice_id._get_currency_rate_date() + price_subtotal_signed = currency._convert( + price_subtotal_signed, + line.invoice_id.company_id.currency_id, + line.company_id or line.env.user.company_id, + date or fields.Date.today()) + sign = line.invoice_id.type in ['in_refund', + 'out_refund'] and -1 or 1 + line.price_subtotal_signed = price_subtotal_signed * sign + else: + super(AccountInvoiceLine, line)._compute_price() diff --git a/sale_order_rental/models/sale_order.py b/sale_order_rental/models/sale_order.py new file mode 100644 index 00000000..711a090a --- /dev/null +++ b/sale_order_rental/models/sale_order.py @@ -0,0 +1,61 @@ +# Copyright 2019 Oihana Larrañaga - AvanzOSC +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +from odoo import api, fields, models + + +class SaleOrder(models.Model): + _inherit = 'sale.order' + + expected_delivery_date = fields.Date( + string='Expected Delivery Date', compute='_compute_rental_days', + inverse='_inverse_expected_dates', store=True) + expected_end_date = fields.Date( + string='Expected Return Date', compute='_compute_rental_days', + inverse='_inverse_expected_dates', store=True) + rental_days = fields.Integer( + string='Rental Days', compute='_compute_rental_days', store=True) + + @api.depends('order_line.expected_delivery_date', + 'order_line.expected_end_date') + def _compute_rental_days(self): + for order in self.filtered('order_line'): + lines = order.order_line.filtered( + lambda l: l.expected_delivery_date and l.expected_end_date) + if lines: + order.expected_delivery_date = min(lines.mapped( + 'expected_delivery_date')) + order.expected_end_date = max(lines.mapped( + 'expected_end_date')) + if order.expected_delivery_date and order.expected_end_date: + order.rental_days = (( + order.expected_end_date - + order.expected_delivery_date).days + 1) + + @api.onchange('expected_delivery_date') + def _onchange_expected_delivery_date(self): + self.commitment_date = (fields.Datetime.to_datetime( + self.expected_delivery_date) if self.expected_delivery_date else + self.commitment_date) + + @api.multi + def _inverse_expected_dates(self): + for order in self: + order_lines = order.order_line.filtered( + lambda l: l.expected_delivery_date and l.expected_end_date) + order_lines.write({ + 'expected_delivery_date': order.expected_delivery_date, + 'expected_end_date': order.expected_end_date, + }) + if order.expected_delivery_date and order.expected_end_date: + order.rental_days = ((order.expected_end_date - + order.expected_delivery_date).days + 1) + + @api.multi + def action_confirm(self): + res = super(SaleOrder, self).action_confirm() + for order in self: + if any(order.mapped('order_line.rental_days')): + for picking in order.picking_ids.filtered( + lambda p: p.state not in ('done', 'cancel')): + picking._return_rental() + return res diff --git a/sale_order_rental/models/sale_order_line.py b/sale_order_rental/models/sale_order_line.py new file mode 100644 index 00000000..e319a598 --- /dev/null +++ b/sale_order_rental/models/sale_order_line.py @@ -0,0 +1,55 @@ +# Copyright 2019 Oihana Larrañaga - AvanzOSC +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class SaleOrderLine(models.Model): + _inherit = 'sale.order.line' + + expected_delivery_date = fields.Date( + string='Expected Delivery Date') + expected_end_date = fields.Date( + string='Expected Return Date') + rental_days = fields.Integer( + string='Rental Days', compute='_compute_rental_days', store=True) + + @api.depends('expected_delivery_date', 'expected_end_date') + def _compute_rental_days(self): + for line in self.filtered( + lambda l: l.expected_delivery_date and l.expected_end_date): + line.rental_days = ( + line.expected_end_date - line.expected_delivery_date).days + 1 + + @api.depends('product_uom_qty', 'discount', 'price_unit', 'tax_id', + 'rental_days') + def _compute_amount(self): + for line in self: + if line.rental_days: + price = line.price_unit * (1 - (line.discount or 0.0) / 100.0) + taxes = line.tax_id.compute_all( + price, line.order_id.currency_id, + line.product_uom_qty * line.rental_days, + product=line.product_id, + partner=line.order_id.partner_shipping_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'], + }) + else: + super(SaleOrderLine, line)._compute_amount() + + @api.constrains('expected_delivery_date', 'expected_end_date') + def _check_expected_dates(self): + for line in self.filtered( + lambda l: l.expected_delivery_date or l.expected_end_date): + if ((line.expected_delivery_date and not line.expected_end_date) + or (line.expected_end_date and + not line.expected_delivery_date)): + raise ValidationError(_('There must be expected delivery ' + 'date and expected end date.')) + if line.expected_delivery_date > line.expected_end_date: + raise ValidationError(_('Expected end date must be after ' + 'expected delivery date.')) diff --git a/sale_order_rental/models/stock_move.py b/sale_order_rental/models/stock_move.py new file mode 100644 index 00000000..64bafae8 --- /dev/null +++ b/sale_order_rental/models/stock_move.py @@ -0,0 +1,49 @@ +# Copyright 2019 Oihana Larrañaga - AvanzOSC +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +from odoo import _, api, fields, models +from odoo.exceptions import ValidationError + + +class StockMove(models.Model): + _inherit = 'stock.move' + + expected_delivery_date = fields.Date( + string='Expected Delivery Date', + related='sale_line_id.expected_delivery_date', + inverse='_inverse_expected_dates', + store=True) + expected_end_date = fields.Date( + string='Expected Return Date', + related='sale_line_id.expected_end_date', + inverse='_inverse_expected_dates', + store=True) + rental_days = fields.Integer( + string='Rental Days', compute='_compute_rental_days', store=True) + + @api.depends('expected_delivery_date', 'expected_end_date') + def _compute_rental_days(self): + for line in self.filtered(lambda l: l.expected_delivery_date and + l.expected_end_date): + line.rental_days = (( + line.expected_end_date - line.expected_delivery_date).days + 1) + + @api.multi + def _inverse_expected_dates(self): + for line in self.filtered('sale_line_id'): + line.sale_line_id.write({ + 'expected_delivery_date': line.expected_delivery_date, + 'expected_end_date': line.expected_end_date, + }) + + @api.constrains('expected_delivery_date', 'expected_end_date') + def _check_expected_dates(self): + for line in self.filtered( + lambda l: l.expected_delivery_date or l.expected_end_date): + if ((line.expected_delivery_date and not line.expected_end_date) + or (line.expected_end_date and not + line.expected_delivery_date)): + raise ValidationError(_('There must be expected delivery ' + 'date and expected end date.')) + if line.expected_delivery_date > line.expected_end_date: + raise ValidationError(_('Expected end date must be after ' + 'expected delivery date.')) diff --git a/sale_order_rental/models/stock_picking.py b/sale_order_rental/models/stock_picking.py new file mode 100644 index 00000000..053fec86 --- /dev/null +++ b/sale_order_rental/models/stock_picking.py @@ -0,0 +1,65 @@ +# Copyright 2019 Oihana Larrañaga - AvanzOSC +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +from odoo import api, fields, models +from odoo.tools.float_utils import float_round + + +class StockPicking(models.Model): + _inherit = 'stock.picking' + + expected_delivery_date = fields.Date( + string='Expected Delivery Date', + related='sale_id.expected_delivery_date') + expected_end_date = fields.Date( + string='Expected Return Date', related='sale_id.expected_end_date') + rental_days = fields.Integer( + related='sale_id.rental_days') + + @api.multi + def _return_rental(self): + self.ensure_one() + if self.state == 'cancel': + return + res = {'picking_id': self.id} + product_return_moves = [] + move_dest_exists = False + for move in self.move_lines.filtered('sale_line_id.rental_days'): + if move.scrapped: + continue + move_dest_exists = bool(move.move_dest_ids) + quantity = move.product_qty - sum(move.move_dest_ids.filtered( + lambda m: m.state in ['partially_available', 'assigned', + 'done']).mapped('move_line_ids').mapped( + 'product_qty')) + quantity = float_round( + quantity, precision_rounding=move.product_uom.rounding) + if quantity: + product_return_moves.append( + (0, 0, {'product_id': move.product_id.id, + 'quantity': quantity, + 'move_id': move.id, + 'uom_id': move.product_id.uom_id.id})) + if not product_return_moves: + return + if self.location_id.usage == 'internal': + res.update({ + 'parent_location_id': + (self.picking_type_id.warehouse_id and + self.picking_type_id.warehouse_id.view_location_id.id or + self.location_id.location_id.id), + }) + location_id = self.location_id.id + if (self.picking_type_id.return_picking_type_id + .default_location_dest_id.return_location): + location_id = ( + self.picking_type_id.return_picking_type_id + .default_location_dest_id.id) + res.update({ + 'product_return_moves': product_return_moves, + 'move_dest_exists': move_dest_exists, + 'original_location_id': self.location_id.id, + 'location_id': location_id, + }) + return_wiz = self.env['stock.return.picking'].create(res) + return_wiz._create_returns() + return True diff --git a/sale_order_rental/security/ir.model.access.csv b/sale_order_rental/security/ir.model.access.csv new file mode 100644 index 00000000..0921b85f --- /dev/null +++ b/sale_order_rental/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink +sale_order_model_manager,sale_order_model_manager,model_sale_order,,1,1,1,1 +sale_order_model_user,sale_order_model_user,model_sale_order,,1,0,0,0 diff --git a/sale_order_rental/static/img/enviado.png b/sale_order_rental/static/img/enviado.png new file mode 100644 index 00000000..412cb943 Binary files /dev/null and b/sale_order_rental/static/img/enviado.png differ diff --git a/sale_order_rental/tests/__init__.py b/sale_order_rental/tests/__init__.py new file mode 100644 index 00000000..c06f7b12 --- /dev/null +++ b/sale_order_rental/tests/__init__.py @@ -0,0 +1,3 @@ +# Copyright 2019 Alfredo de la Fuente - AvanzOSC +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +from . import test_sale_order_rental diff --git a/sale_order_rental/tests/common.py b/sale_order_rental/tests/common.py new file mode 100644 index 00000000..6fae4b7f --- /dev/null +++ b/sale_order_rental/tests/common.py @@ -0,0 +1,44 @@ +# Copyright 2019 Oihana Larrañaga - AvanzOSC +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +from odoo.tests import common +from odoo import fields +from dateutil.relativedelta import relativedelta + + +class SaleOrderRentalCommon(common.SavepointCase): + + @classmethod + def setUpClass(cls): + super(SaleOrderRentalCommon, cls).setUpClass() + cls.invoice_model = cls.env['account.invoice'] + + cls.sale_model = cls.env['sale.order'].with_context( + tracking_disable=True) + cls.product = cls.env['product.product'].create({ + 'name': 'Test Product', + 'type': 'consu', + }) + # cls.service_product = cls.env.ref('product.product_delivery_02') + cls.customer = cls.env.ref('base.res_partner_1') + cond = [('type_tax_use', '=', 'sale'), + ('amount', '>', 1)] + cls.tax = cls.env['account.tax'].search(cond, limit=1) + cls.delivery_date = fields.Date.today() + cls.end_date = cls.delivery_date + relativedelta(days=+6) + cls.sale_order = cls.sale_model.create({ + 'partner_id': cls.customer.id, + 'order_line': [(0, 0, { + 'product_id': cls.product.id, + 'name': cls.product.name, + 'product_uom_qty': 2, + 'product_uom': cls.product.uom_id.id, + 'price_unit': cls.product.list_price, + 'expected_delivery_date': cls.delivery_date, + 'expected_end_date': cls.end_date, + 'tax_id': [(6, 0, cls.tax.ids)], + })] + }) + cls.sale_order.warehouse_id.out_type_id.return_picking_type_id.\ + default_location_dest_id.write({ + 'return_location': True, + }) diff --git a/sale_order_rental/tests/test_sale_order_rental.py b/sale_order_rental/tests/test_sale_order_rental.py new file mode 100644 index 00000000..998537fd --- /dev/null +++ b/sale_order_rental/tests/test_sale_order_rental.py @@ -0,0 +1,114 @@ +# Copyright 2019 Oihana Larrañaga - AvanzOSC +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +from .common import SaleOrderRentalCommon +from odoo.tests import common +from odoo.exceptions import ValidationError +from dateutil.relativedelta import relativedelta + + +@common.at_install(False) +@common.post_install(True) +class TestSaleOrderRental(SaleOrderRentalCommon): + + def test_sale_rental_sale_order(self): + self.assertEquals(self.sale_order.rental_days, 7) + new_delivery_date = self.delivery_date - relativedelta(days=+1) + new_end_date = self.end_date + relativedelta(days=+1) + self.sale_order.write({ + 'expected_delivery_date': new_delivery_date, + 'expected_end_date': new_end_date, + }) + for line in self.sale_order.order_line: + self.assertNotEquals( + line.expected_delivery_date, self.delivery_date) + self.assertEquals( + line.expected_delivery_date, new_delivery_date) + self.assertNotEquals( + line.expected_end_date, self.end_date) + self.assertEquals(line.expected_end_date, new_end_date) + with self.assertRaises(ValidationError): + line.write({ + 'expected_delivery_date': False, + }) + with self.assertRaises(ValidationError): + line.write({ + 'expected_delivery_date': self.end_date, + 'expected_end_date': self.delivery_date, + }) + self.assertEquals(self.sale_order.rental_days, 9) + + def test_sale_order_rental(self): + for line in self.sale_order.order_line: + self.assertEquals(line.expected_delivery_date, + self.sale_order.expected_delivery_date) + self.assertEquals(line.expected_end_date, + self.sale_order.expected_end_date) + self.sale_order._onchange_expected_delivery_date() + self.assertEquals(self.sale_order.commitment_date.date(), + self.sale_order.expected_delivery_date) + self.assertEquals(self.sale_order.rental_days, 7) + self.sale_order.action_confirm() + self.assertEquals(len(self.sale_order.picking_ids), 2) + out_picking = self.sale_order.picking_ids.filtered( + lambda p: p.picking_type_code == 'outgoing') + return_picking = self.sale_order.picking_ids.filtered( + lambda p: p.picking_type_code == 'incoming') + self.assertEquals( + out_picking.location_id, return_picking.location_dest_id) + self.assertEquals( + out_picking.picking_type_id, + return_picking.picking_type_id.return_picking_type_id) + for move in out_picking.move_lines: + self.assertEquals( + move.expected_delivery_date, + move.sale_line_id.expected_delivery_date) + self.assertEquals( + move.expected_end_date, + move.sale_line_id.expected_end_date) + self.assertEquals( + move.rental_days, + move.sale_line_id.rental_days) + with self.assertRaises(ValidationError): + move.write({ + 'expected_delivery_date': + move.sale_line_id.expected_end_date, + 'expected_end_date': + move.sale_line_id.expected_delivery_date, + }) + with self.assertRaises(ValidationError): + move.write({ + 'expected_delivery_date': False, + }) + move.write({ + 'expected_delivery_date': ( + self.delivery_date - relativedelta(days=+1)), + 'expected_end_date': ( + self.end_date + relativedelta(days=+1)), + }) + self.assertEquals( + move.sale_line_id.rental_days, 9) + out_picking.action_confirm() + for move in out_picking.move_lines: + move.move_line_ids.write({ + 'qty_done': move.product_uom_qty, + }) + out_picking._return_rental() # It won't be able to create return + out_picking.action_done() + invoice_ids = self.sale_order.action_invoice_create() + self.assertEquals(len(invoice_ids), 1) + for invoice in self.invoice_model.browse(invoice_ids): + self.assertEquals(self.sale_order.expected_delivery_date, + invoice.sale_expected_delivery_date) + self.assertEquals(self.sale_order.expected_end_date, + invoice.sale_expected_end_date) + self.assertEquals(self.sale_order.rental_days, invoice.rental_days) + for line in self.sale_order.order_line: + for invoice_line in line.invoice_lines: + self.assertEquals( + line.expected_delivery_date, + invoice_line.sale_expected_delivery_date) + self.assertEquals( + line.expected_end_date, + invoice_line.sale_expected_end_date) + self.assertEquals( + line.rental_days, invoice_line.rental_days) diff --git a/sale_order_rental/views/account_invoice_view.xml b/sale_order_rental/views/account_invoice_view.xml new file mode 100644 index 00000000..1dd93508 --- /dev/null +++ b/sale_order_rental/views/account_invoice_view.xml @@ -0,0 +1,19 @@ + + + + account.invoice + + + + + + + + + + + + + + + diff --git a/sale_order_rental/views/sale_order_view.xml b/sale_order_rental/views/sale_order_view.xml new file mode 100644 index 00000000..636f79da --- /dev/null +++ b/sale_order_rental/views/sale_order_view.xml @@ -0,0 +1,56 @@ + + + + sale.order + + + + + + + + + + + + sale.order + + + + + + + + + + + + sale.order + + + + + + + + + { + 'default_expected_delivery_date': + expected_delivery_date, + 'default_expected_end_date': expected_end_date + } + + + + + + + + + + + + + + diff --git a/sale_order_rental/views/stock_picking_view.xml b/sale_order_rental/views/stock_picking_view.xml new file mode 100644 index 00000000..6c0aaab3 --- /dev/null +++ b/sale_order_rental/views/stock_picking_view.xml @@ -0,0 +1,30 @@ + + + + stock.picking + + + + + + + + + + + + + + + + stock.picking + + + + + + + + + + diff --git a/sale_order_rental/wizards/__init__.py b/sale_order_rental/wizards/__init__.py new file mode 100644 index 00000000..ad0b47c2 --- /dev/null +++ b/sale_order_rental/wizards/__init__.py @@ -0,0 +1 @@ +from . import stock_picking_return diff --git a/sale_order_rental/wizards/stock_picking_return.py b/sale_order_rental/wizards/stock_picking_return.py new file mode 100644 index 00000000..9cba4ad5 --- /dev/null +++ b/sale_order_rental/wizards/stock_picking_return.py @@ -0,0 +1,17 @@ +# Copyright 2019 Oihana Larrañaga - AvanzOSC +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html + +from odoo import fields, models + + +class ReturnPicking(models.TransientModel): + _inherit = 'stock.return.picking' + + def _prepare_move_default_values(self, return_line, new_picking): + vals = super(ReturnPicking, self)._prepare_move_default_values( + return_line, new_picking) + vals.update({ + 'date_expected': fields.Datetime.to_datetime( + self.picking_id.sale_id.expected_end_date), + }) + return vals