diff --git a/sale_order_partials/README.rst b/sale_order_partials/README.rst new file mode 100644 index 000000000..1c65b59de --- /dev/null +++ b/sale_order_partials/README.rst @@ -0,0 +1,17 @@ +.. 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 + +=================== +Partial Sale Orders +=================== + +Creates related sale orders + +Credits +======= + +Contributors +------------ +* Mikel Arregi +* Ana Juaristi diff --git a/sale_order_partials/__init__.py b/sale_order_partials/__init__.py new file mode 100644 index 000000000..5ac02eccf --- /dev/null +++ b/sale_order_partials/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Mikel Arregi Etxaniz - AvanzOSC +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +from . import models +from . import wizard \ No newline at end of file diff --git a/sale_order_partials/__openerp__.py b/sale_order_partials/__openerp__.py new file mode 100644 index 000000000..4687e7fdb --- /dev/null +++ b/sale_order_partials/__openerp__.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# © Copyright 2018 Mikel Arregi Etxaniz - AvanzOSC +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +{ + "name": "Partial sale orders", + "version": "8.0.1.0.0", + "license": "AGPL-3", + "depends": [ + "sale", + "stock", + "sale_order_type", + ], + "author": "OdooMRP team, " + "AvanzOSC, " + "Odoo Community Association (OCA)", + "website": "http://www.odoomrp.com", + "contributors": [ + "Mikel Arregi ", + "Ana Juaristi ", + ], + "category": "", + "summary": "", + "data": [ + "views/sale_order_view.xml", + "views/res_config_view.xml", + "wizard/duplicate_upgradable_sale_view.xml", + ], + "installable": True, +} diff --git a/sale_order_partials/models/__init__.py b/sale_order_partials/models/__init__.py new file mode 100644 index 000000000..364132b83 --- /dev/null +++ b/sale_order_partials/models/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Mikel Arregi Etxaniz - AvanzOSC +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +from . import sale +from . import res_config diff --git a/sale_order_partials/models/res_config.py b/sale_order_partials/models/res_config.py new file mode 100644 index 000000000..c2358ff62 --- /dev/null +++ b/sale_order_partials/models/res_config.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Mikel Arregi Etxaniz - AvanzOSC +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +from openerp import api, fields, models + + +class SaleConfigSettings(models.TransientModel): + _inherit = 'sale.config.settings' + + sale_type_id = fields.Many2one(comodel_name='sale.order.type', + string="Partial orders type") + + def _get_parameter(self, key, default=False): + param_obj = self.env['ir.config_parameter'] + rec = param_obj.search([('key', '=', key)]) + return rec or default + + def _write_or_create_param(self, key, value): + param_obj = self.env['ir.config_parameter'] + rec = self._get_parameter(key) + if rec: + if not value: + rec.unlink() + else: + rec.value = value + elif value: + param_obj.create({'key': key, 'value': value}) + + @api.multi + def get_default_parameters(self): + def get_value(key, default=''): + rec = self._get_parameter(key) + return rec and rec.value or default + return { + 'sale_type_id': get_value('sale.type.id', False), + } + + @api.multi + def set_parameters(self): + self._write_or_create_param('sale.type.id', self.sale_type_id.id) diff --git a/sale_order_partials/models/sale.py b/sale_order_partials/models/sale.py new file mode 100644 index 000000000..c5d73d182 --- /dev/null +++ b/sale_order_partials/models/sale.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Mikel Arregi Etxaniz - AvanzOSC +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +from openerp import api, exceptions, fields, models, _ +from openerp.addons import decimal_precision as dp + + +class SaleOrder(models.Model): + _inherit = "sale.order" + + child_order_ids = fields.One2many(comodel_name="sale.order", + inverse_name="parent_order_id", + string="Sales") + parent_order_id = fields.Many2one(comodel_name="sale.order") + served_quantity = fields.Float(digits=dp.get_precision('Product Unit of ' + 'Measure'), + compute="_compute_line_quantities") + not_served_quantity = fields.Float(digits=dp.get_precision( + 'Product Unit of Measure'), compute="_compute_line_quantities") + reserved_qty = fields.Float(digits=dp.get_precision( + 'Product Unit of Measure'), compute="_compute_line_quantities") + served_quantity_percentage = fields.Float(digits=dp.get_precision( + 'Product Unit of Measure'), compute="_compute_line_quantities") + upgrade = fields.Boolean(string="Upgrade", copy=False) + + @api.multi + def action_open_partial_sales(self): + template_obj = self.env['product.template'] + result = template_obj._get_act_window_dict('sale.action_orders') + result['domain'] = "[('parent_order_id', '=', %d)]" % self.id + result['context'] = {'search_default_internal_loc': 1} + return result + + @api.depends("order_line") + def _compute_line_quantities(self): + for record in self: + if record.upgrade and record.order_line: + total = record.order_line[0].product_uom_qty + moves = record.child_order_ids.mapped( + 'picking_ids.move_lines').filtered( + lambda x: x.state == 'done') + served_qty = sum(moves.mapped("product_uom_qty")) + not_served_qty = total - served_qty + record.served_quantity = served_qty + record.reserved_qty = record.reserved_child_qty() - served_qty + record.not_served_quantity = not_served_qty + record.served_quantity_percentage = ( + served_qty / total * 100) + + @api.constrains("order_line", "upgrade") + def check_sale_upgradable_has_one_line(self): + if self.upgrade and len(self.order_line) > 1: + raise exceptions.Warning('Sale upgrades can only have one ' + 'order line') + + @api.multi + def action_create_order_from_upgrade(self, qty=None): + param_obj = self.env['sale.config.settings'] + sale_type_param = param_obj._get_parameter('sale.type.id') + sale_type = sale_type_param and sale_type_param.value + for record in self: + if record.upgrade and record.invoiced: + new_record = record.copy({'parent_order_id': record.id, + 'type_id': int(sale_type)}) + if qty: + new_record.order_line[0].write({'product_uom_qty': qty, + 'discount': 100.}) + elif not record.upgrade: + raise exceptions.Warning( + _("The order %s is not upgradable. Edit order and check " + "'Upgrade' field") % record.name) + else: + raise exceptions.Warning(_("There are unpaid invoices")) + + @api.multi + def reserved_child_qty(self): + self.ensure_one() + not_canceled_children = self.child_order_ids.filtered( + lambda x: x.state != "cancel") + reserved_qty = sum(not_canceled_children.mapped( + "order_line.product_uom_qty")) + return reserved_qty diff --git a/sale_order_partials/tests/__init__.py b/sale_order_partials/tests/__init__.py new file mode 100644 index 000000000..23e7f2417 --- /dev/null +++ b/sale_order_partials/tests/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Mikel Arregi Etxaniz - AvanzOSC +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +from . import test_sale_order_partials diff --git a/sale_order_partials/tests/test_sale_order_partials.py b/sale_order_partials/tests/test_sale_order_partials.py new file mode 100644 index 000000000..1f46dac5e --- /dev/null +++ b/sale_order_partials/tests/test_sale_order_partials.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Mikel Arregi Etxaniz - AvanzOSC +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html + +import openerp.tests.common as common +from openerp import exceptions + + +class SaleOrderPartials(common.SavepointCase): + + @classmethod + def setUpClass(cls): + super(SaleOrderPartials, cls).setUpClass() + cls.wiz_obj = cls.env['duplicate.upgradable.sale'] + cls.partner = cls.env['res.partner'].create({ + 'name': 'Partner to test', + }) + cls.product = cls.env['product.product'].create({ + 'name': 'Product to test' + }) + cls.sale_order = cls.env['sale.order'].create({ + 'partner_id': cls.partner.id, + 'upgrade': True, + 'order_line': [(0, 0, {'product_id': cls.product.id, + 'product_uom_qty': 100}, )], + }) + + def test_partials(self): + wiz = self.wiz_obj.with_context(active_ids=[ + self.sale_order.id]).create({ + 'quantity': 5}) + with self.assertRaises(exceptions.Warning): + wiz.action_duplicate() diff --git a/sale_order_partials/views/res_config_view.xml b/sale_order_partials/views/res_config_view.xml new file mode 100644 index 000000000..e5bb8e875 --- /dev/null +++ b/sale_order_partials/views/res_config_view.xml @@ -0,0 +1,18 @@ + + + + + view.sale.upgradable.type + sale.config.settings + + + +
+
+
+
+
+
+
diff --git a/sale_order_partials/views/sale_order_view.xml b/sale_order_partials/views/sale_order_view.xml new file mode 100644 index 000000000..a814e8b4a --- /dev/null +++ b/sale_order_partials/views/sale_order_view.xml @@ -0,0 +1,122 @@ + + + + + + upgradable.sale.order.search + sale.order + + + + + + + + + + + + + + + + + upgradable.sale.order.form + sale.order + + + + + + + + + + + + + + + + + sale.order.buttons + sale.order + + +

+
+
+

+
+
+ + + upgradable.sale.order.button.form + sale.order + + +
+ +
+
+
+ + + + + upgradable.sale.order.tree + sale.order + + + + + + + + + + + + + + tf.view.picking.search + stock.picking + + + + + + + + + + + + +
+
diff --git a/sale_order_partials/wizard/__init__.py b/sale_order_partials/wizard/__init__.py new file mode 100644 index 000000000..98941b02c --- /dev/null +++ b/sale_order_partials/wizard/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Mikel Arregi Etxaniz - AvanzOSC +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +from . import duplicate_upgradable_sal diff --git a/sale_order_partials/wizard/duplicate_upgradable_sal.py b/sale_order_partials/wizard/duplicate_upgradable_sal.py new file mode 100644 index 000000000..df27d59cc --- /dev/null +++ b/sale_order_partials/wizard/duplicate_upgradable_sal.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 Mikel Arregi Etxaniz - AvanzOSC +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html +from openerp import api, exceptions, fields, models, _ +from openerp.addons import decimal_precision as dp + + +class DuplicateUpgradableSale(models.TransientModel): + _name = "duplicate.upgradable.sale" + + in_process_quantity = fields.Float(digits=dp.get_precision( + 'Product Unit of Measure')) + not_reserved_quantity = fields.Float(digits=dp.get_precision( + 'Product Unit of Measure')) + quantity = fields.Float(digits=dp.get_precision( + 'Product Unit of Measure')) + + @api.model + def default_get(self, var_fields): + res = super(DuplicateUpgradableSale, self).default_get(var_fields) + order = self.env['sale.order'].browse(self._context.get('active_ids')) + order.ensure_one() + if not order.upgrade: + raise exceptions.Warning(_("Order not upgradable")) + reserved_qty = order.reserved_child_qty() + total = order.order_line[0].product_uom_qty + res.update( + {'in_process_quantity': reserved_qty, + 'not_reserved_quantity': total - reserved_qty} + ) + return res + + @api.multi + def action_duplicate(self): + orders = self.env['sale.order'].browse(self._context.get('active_ids')) + orders.ensure_one() + quantity = self.quantity or self.not_reserved_quantity or False + if not quantity or quantity > self.not_reserved_quantity: + raise exceptions.Warning(_("Quantity is excessive or order is " + "fully served")) + orders.action_create_order_from_upgrade(quantity) diff --git a/sale_order_partials/wizard/duplicate_upgradable_sale_view.xml b/sale_order_partials/wizard/duplicate_upgradable_sale_view.xml new file mode 100644 index 000000000..1c21017be --- /dev/null +++ b/sale_order_partials/wizard/duplicate_upgradable_sale_view.xml @@ -0,0 +1,35 @@ + + + + + duplicate.upgradable.sale.form + duplicate.upgradable.sale + +
+ + + + + +
+
+
+
+
+ + + +
+