From 5c7792253337e84ed329fef7727b08419b7ad5c9 Mon Sep 17 00:00:00 2001 From: andreparames Date: Tue, 23 Jan 2018 11:17:03 +0100 Subject: [PATCH 01/57] [10.0][ADD] sale_blanket_orders --- sale_blanket_order/README.rst | 69 ++++ sale_blanket_order/__init__.py | 2 + sale_blanket_order/__manifest__.py | 28 ++ sale_blanket_order/data/ir_cron.xml | 20 ++ sale_blanket_order/data/sequence.xml | 13 + sale_blanket_order/models/__init__.py | 2 + sale_blanket_order/models/blanket_orders.py | 321 ++++++++++++++++++ sale_blanket_order/models/sale_orders.py | 18 + sale_blanket_order/report/report.xml | 11 + sale_blanket_order/report/templates.xml | 74 ++++ .../security/ir.model.access.csv | 7 + sale_blanket_order/security/security.xml | 19 ++ sale_blanket_order/tests/__init__.py | 1 + .../tests/test_blanket_orders.py | 85 +++++ sale_blanket_order/views/blanket_orders.xml | 122 +++++++ sale_blanket_order/views/sale_orders.xml | 50 +++ sale_blanket_order/wizard/__init__.py | 1 + .../wizard/create_sale_orders.py | 111 ++++++ .../wizard/create_sale_orders.xml | 34 ++ 19 files changed, 988 insertions(+) create mode 100644 sale_blanket_order/README.rst create mode 100644 sale_blanket_order/__init__.py create mode 100644 sale_blanket_order/__manifest__.py create mode 100644 sale_blanket_order/data/ir_cron.xml create mode 100644 sale_blanket_order/data/sequence.xml create mode 100644 sale_blanket_order/models/__init__.py create mode 100644 sale_blanket_order/models/blanket_orders.py create mode 100644 sale_blanket_order/models/sale_orders.py create mode 100644 sale_blanket_order/report/report.xml create mode 100644 sale_blanket_order/report/templates.xml create mode 100644 sale_blanket_order/security/ir.model.access.csv create mode 100644 sale_blanket_order/security/security.xml create mode 100644 sale_blanket_order/tests/__init__.py create mode 100644 sale_blanket_order/tests/test_blanket_orders.py create mode 100644 sale_blanket_order/views/blanket_orders.xml create mode 100644 sale_blanket_order/views/sale_orders.xml create mode 100644 sale_blanket_order/wizard/__init__.py create mode 100644 sale_blanket_order/wizard/create_sale_orders.py create mode 100644 sale_blanket_order/wizard/create_sale_orders.xml diff --git a/sale_blanket_order/README.rst b/sale_blanket_order/README.rst new file mode 100644 index 00000000000..a8228489828 --- /dev/null +++ b/sale_blanket_order/README.rst @@ -0,0 +1,69 @@ +.. image:: https://img.shields.io/badge/license-AGPL--3-blue.png + :target: https://www.gnu.org/licenses/agpl + :alt: License: AGPL-3 + +============== +Blanket Orders +============== + +A blanket order is a pre-agreement to sell a certain number of quantities of +products at a specific price. From a confirmed blanket order, the users can +create new sale orders at such price, until the blanket order expires, either +due to reaching the validity date or exhausting all the quantities of products. + +Usage +===== + +A new menu in the Sale area is created, allowing users to create new blanket +orders. + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/167/11.0 + + + +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 +======= + +Images +------ + +* Odoo Community Association: `Icon `_. + +Contributors +------------ + +* André Pereira (https://www.acsone.eu/) + +Do not contact contributors directly about support or help with technical issues. + +Funders +------- + +The development of this module has been financially supported by: + +* Acsone SA/NV + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://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 https://odoo-community.org. diff --git a/sale_blanket_order/__init__.py b/sale_blanket_order/__init__.py new file mode 100644 index 00000000000..9b4296142f4 --- /dev/null +++ b/sale_blanket_order/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import wizard diff --git a/sale_blanket_order/__manifest__.py b/sale_blanket_order/__manifest__.py new file mode 100644 index 00000000000..7579b7177f5 --- /dev/null +++ b/sale_blanket_order/__manifest__.py @@ -0,0 +1,28 @@ +# coding: utf-8 +# Copyright 2018 Acsone +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). +{ + 'name': 'Blanket Orders', + 'category': 'Sale', + 'license': 'AGPL-3', + 'author': 'Acsone SA/NV,Odoo Community Association (OCA)', + 'version': '10.0.1.0.0', + 'website': 'https://github.com/OCA/sale-workflow', + 'summary': "Blanket Orders", + 'depends': [ + 'sale', + 'web_action_conditionable', + ], + 'data': [ + 'data/sequence.xml', + 'data/ir_cron.xml', + 'security/ir.model.access.csv', + 'security/security.xml', + 'wizard/create_sale_orders.xml', + 'views/blanket_orders.xml', + 'views/sale_orders.xml', + 'report/templates.xml', + 'report/report.xml', + ], + 'installable': True, +} diff --git a/sale_blanket_order/data/ir_cron.xml b/sale_blanket_order/data/ir_cron.xml new file mode 100644 index 00000000000..cd6453937cd --- /dev/null +++ b/sale_blanket_order/data/ir_cron.xml @@ -0,0 +1,20 @@ + + + + + + + Expire Blanket Orders + 1 + days + + -1 + + sale.blanket.order + expire_orders + () + + + diff --git a/sale_blanket_order/data/sequence.xml b/sale_blanket_order/data/sequence.xml new file mode 100644 index 00000000000..63264eeeccc --- /dev/null +++ b/sale_blanket_order/data/sequence.xml @@ -0,0 +1,13 @@ + + + + + + Blanket Order + sale.blanket.order + BO + 3 + + + + diff --git a/sale_blanket_order/models/__init__.py b/sale_blanket_order/models/__init__.py new file mode 100644 index 00000000000..36b4289c0ec --- /dev/null +++ b/sale_blanket_order/models/__init__.py @@ -0,0 +1,2 @@ +from . import blanket_orders +from . import sale_orders diff --git a/sale_blanket_order/models/blanket_orders.py b/sale_blanket_order/models/blanket_orders.py new file mode 100644 index 00000000000..284345e91bf --- /dev/null +++ b/sale_blanket_order/models/blanket_orders.py @@ -0,0 +1,321 @@ +# coding: utf-8 +# Copyright 2018 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import fields, models, api, _ +from odoo.exceptions import Warning +from odoo.tools import float_is_zero + + +class BlanketOrder(models.Model): + _name = 'sale.blanket.order' + _inherit = ['mail.thread', 'ir.needaction_mixin'] + _description = 'Blanket Order' + + @api.model + def _get_default_team(self): + return self.env['crm.team']._get_default_team_id() + + @api.model + def _default_currency(self): + return self.env.user.company_id.currency_id + + @api.model + def _default_company(self): + return self.env.user.company_id + + name = fields.Char( + default='Draft', + readonly=True + ) + partner_id = fields.Many2one( + 'res.partner', string='Partner', readonly=True, + states={'draft': [('readonly', False)]}) + lines_ids = fields.One2many( + 'sale.blanket.order.line', 'order_id', string='Order lines') + pricelist_id = fields.Many2one( + 'product.pricelist', string='Pricelist', required=True, readonly=True, + states={'draft': [('readonly', False)]}) + currency_id = fields.Many2one( + 'res.currency', related='pricelist_id.currency_id', readonly=True) + payment_term_id = fields.Many2one( + 'account.payment.term', string='Payment Terms', readonly=True, + states={'draft': [('readonly', False)]}) + confirmed = fields.Boolean() + state = fields.Selection(selection=[ + ('draft', 'Draft'), + ('opened', 'Opened'), + ('expired', 'Expired'), + ], compute='_compute_state', store=True) + validity_date = fields.Date( + readonly=True, + states={'draft': [('readonly', False)]}) + client_order_ref = fields.Char( + string='Customer Reference', copy=False, readonly=True, + states={'draft': [('readonly', False)]}) + note = fields.Text( + readonly=True, + states={'draft': [('readonly', False)]}) + user_id = fields.Many2one( + 'res.users', string='Salesperson', readonly=True, + states={'draft': [('readonly', False)]}) + team_id = fields.Many2one( + 'crm.team', string='Sales Team', change_default=True, + default=_get_default_team, readonly=True, + states={'draft': [('readonly', False)]}) + company_id = fields.Many2one( + 'res.company', string='Company', default=_default_company, + readonly=True, + states={'draft': [('readonly', False)]}) + sale_count = fields.Integer(compute='_compute_sale_count') + + @api.multi + def _get_sale_orders(self): + lines = self.mapped('lines_ids') + sale_lines = lines.mapped('sale_order_lines_ids') + sale_orders = sale_lines.mapped('order_id') + sale_orders_ids = list(set(sale_orders.ids)) + return self.env['sale.order'].browse(sale_orders_ids) + + @api.multi + @api.depends('lines_ids.remaining_qty') + def _compute_sale_count(self): + for blanket_order in self: + blanket_order.sale_count = len(blanket_order._get_sale_orders()) + + @api.multi + @api.depends( + 'lines_ids.remaining_qty', + 'validity_date', + 'confirmed' + ) + def _compute_state(self): + today = fields.Date.today() + precision = self.env['decimal.precision'].precision_get( + 'Product Unit of Measure') + for order in self: + if not order.confirmed: + order.state = 'draft' + elif order.validity_date <= today: + order.state = 'expired' + elif float_is_zero(sum(order.lines_ids.mapped('remaining_qty')), + precision_digits=precision): + order.state = 'expired' + else: + order.state = 'opened' + + @api.multi + @api.onchange('partner_id') + def onchange_partner_id(self): + """ + Update the following fields when the partner is changed: + - Pricelist + - Payment term + """ + if not self.partner_id: + self.payment_term_id = False + return + + values = { + 'pricelist_id': (self.partner_id.property_product_pricelist and + self.partner_id.property_product_pricelist.id or + False), + 'payment_term_id': (self.partner_id.property_payment_term_id and + self.partner_id.property_payment_term_id.id or + False), + } + + if self.partner_id.user_id: + values['user_id'] = self.partner_id.user_id.id + if self.partner_id.team_id: + values['team_id'] = self.partner_id.team_id.id + self.update(values) + + @api.multi + def _validate(self): + try: + today = fields.Date.today() + for order in self: + assert order.validity_date, _("Validity date is mandatory") + assert order.validity_date > today, \ + _("Validity date must be in the future") + assert order.partner_id, _("Partner is mandatory") + assert len(order.lines_ids) > 0, _("Must have some lines") + order.lines_ids._validate() + except AssertionError as e: + raise Warning(e.message) + + @api.multi + def action_confirm(self): + self._validate() + for order in self: + sequence_obj = self.env['ir.sequence'] + if order.company_id: + sequence_obj = sequence_obj.with_context( + force_company=order.company_id.id) + name = sequence_obj.next_by_code('sale.blanket.order') + order.write({'confirmed': True, 'name': name}) + return True + + @api.multi + def action_view_sale_orders(self): + sale_orders = self._get_sale_orders() + action = self.env.ref('sale.action_orders').read()[0] + if len(sale_orders) > 0: + action['domain'] = [('id', 'in', sale_orders.ids)] + else: + action = {'type': 'ir.actions.act_window_close'} + return action + + @api.model + def expire_orders(self): + today = fields.Date.today() + expired_orders = self.search([ + ('state', '=', 'opened'), + ('validity_date', '<=', today), + ]) + expired_orders.modified(['validity_date']) + expired_orders.recompute() + + +class BlanketOrderLine(models.Model): + _name = 'sale.blanket.order.line' + _description = 'Blanket Order Line' + + sequence = fields.Integer() + order_id = fields.Many2one('sale.blanket.order', required=True) + product_id = fields.Many2one( + 'product.product', string='Product', required=True) + product_uom = fields.Many2one( + 'product.uom', string='Unit of Measure', required=True) + price_unit = fields.Float(string='Price', required=True) + original_qty = fields.Float( + string='Original quantity', required=True, default=1) + ordered_qty = fields.Float( + string='Ordered quantity', compute='_compute_quantities', store=True) + invoiced_qty = fields.Float( + string='Invoiced quantity', compute='_compute_quantities', store=True) + remaining_qty = fields.Float( + string='Remaining quantity', compute='_compute_quantities', store=True) + delivered_qty = fields.Float( + string='Delivered quantity', compute='_compute_quantities', store=True) + sale_order_lines_ids = fields.One2many( + 'sale.order.line', 'blanket_line_id', string='Sale order lines') + company_id = fields.Many2one( + 'res.company', related='order_id.company_id', store=True) + + def _get_real_price_currency(self, product, rule_id, qty, uom, + pricelist_id): + """Retrieve the price before applying the pricelist + :param obj product: object of current product record + :parem float qty: total quentity of product + :param tuple price_and_rule: tuple(price, suitable_rule) coming + from pricelist computation + :param obj uom: unit of measure of current order line + :param integer pricelist_id: pricelist id of sale order""" + # Copied and adapted from the sale module + PricelistItem = self.env['product.pricelist.item'] + field_name = 'lst_price' + currency_id = None + product_currency = None + if rule_id: + pricelist_item = PricelistItem.browse(rule_id) + if pricelist_item.pricelist_id\ + .discount_policy == 'without_discount': + while pricelist_item.base == 'pricelist'\ + and pricelist_item.base_pricelist_id\ + and pricelist_item.base_pricelist_id\ + .discount_policy == 'without_discount': + price, rule_id = pricelist_item.base_pricelist_id\ + .with_context(uom=uom.id).get_product_price_rule( + product, qty, self.order_id.partner_id) + pricelist_item = PricelistItem.browse(rule_id) + + if pricelist_item.base == 'standard_price': + field_name = 'standard_price' + if pricelist_item.base == 'pricelist'\ + and pricelist_item.base_pricelist_id: + field_name = 'price' + product = product.with_context( + pricelist=pricelist_item.base_pricelist_id.id) + product_currency = pricelist_item.base_pricelist_id.currency_id + currency_id = pricelist_item.pricelist_id.currency_id + + product_currency = (product_currency or + (product.company_id and + product.company_id.currency_id) or + self.env.user.company_id.currency_id) + if not currency_id: + currency_id = product_currency + cur_factor = 1.0 + else: + if currency_id.id == product_currency.id: + cur_factor = 1.0 + else: + cur_factor = currency_id._get_conversion_rate( + product_currency, currency_id) + + product_uom = product.uom_id.id + if uom and uom.id != product_uom: + # the unit price is in a different uom + uom_factor = uom._compute_price(1.0, product.uom_id) + else: + uom_factor = 1.0 + + return product[field_name] * uom_factor * cur_factor, currency_id.id + + @api.multi + def _get_display_price(self, product): + # Copied and adapted from the sale module + pricelist = self.order_id.pricelist_id + partner = self.order_id.partner_id + if self.order_id.pricelist_id.discount_policy == 'with_discount': + return product.with_context(pricelist=pricelist.id).price + final_price, rule_id = pricelist.get_product_price_rule( + self.product_id, self.original_qty or 1.0, partner) + context_partner = dict(self.env.context, partner_id=partner.id, + date=fields.Date.today()) + base_price, currency_id = self.with_context(context_partner)\ + ._get_real_price_currency( + self.product_id, rule_id, self.original_qty, self.product_uom, + pricelist.id) + if currency_id != pricelist.currency_id.id: + currency = self.env['res.currency'].browse(currency_id) + base_price = currency.with_context(context_partner).compute( + base_price, pricelist.currency_id) + # negative discounts (= surcharge) are included in the display price + return max(base_price, final_price) + + @api.multi + @api.onchange('product_id') + def onchange_product(self): + if self.product_id: + self.product_uom = self.product_id.uom_id.id + if self.order_id.pricelist_id and self.order_id.partner_id: + self.price_unit = self._get_display_price(self.product_id) + + @api.multi + @api.depends( + 'sale_order_lines_ids.blanket_line_id', + 'sale_order_lines_ids.product_uom_qty', + 'sale_order_lines_ids.qty_delivered', + 'sale_order_lines_ids.qty_invoiced', + 'original_qty' + ) + def _compute_quantities(self): + for line in self: + sale_lines = line.sale_order_lines_ids + line.ordered_qty = sum(l.product_uom_qty for l in sale_lines) + line.invoiced_qty = sum(l.qty_invoiced for l in sale_lines) + line.delivered_qty = sum(l.qty_delivered for l in sale_lines) + line.remaining_qty = line.original_qty - line.ordered_qty + + @api.multi + def _validate(self): + try: + for line in self: + assert line.price_unit > 0.0, \ + _("Price must be greater than zero") + assert line.original_qty > 0.0, \ + _("Quantity must be greater than zero") + except AssertionError as e: + raise Warning(e.message) diff --git a/sale_blanket_order/models/sale_orders.py b/sale_blanket_order/models/sale_orders.py new file mode 100644 index 00000000000..448b4d69ad7 --- /dev/null +++ b/sale_blanket_order/models/sale_orders.py @@ -0,0 +1,18 @@ +# coding: utf-8 +# Copyright 2018 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import fields, models + + +class SaleOrder(models.Model): + _inherit = 'sale.order' + + blanket_order_id = fields.Many2one( + 'sale.blanket.order', string='Origin blanket order', + related='order_line.blanket_line_id.order_id') + + +class SaleOrderLine(models.Model): + _inherit = 'sale.order.line' + + blanket_line_id = fields.Many2one('sale.blanket.order.line') diff --git a/sale_blanket_order/report/report.xml b/sale_blanket_order/report/report.xml new file mode 100644 index 00000000000..964c11250a9 --- /dev/null +++ b/sale_blanket_order/report/report.xml @@ -0,0 +1,11 @@ + + + + diff --git a/sale_blanket_order/report/templates.xml b/sale_blanket_order/report/templates.xml new file mode 100644 index 00000000000..48731b3f4e0 --- /dev/null +++ b/sale_blanket_order/report/templates.xml @@ -0,0 +1,74 @@ + + + + + + + diff --git a/sale_blanket_order/security/ir.model.access.csv b/sale_blanket_order/security/ir.model.access.csv new file mode 100644 index 00000000000..06df5441fc1 --- /dev/null +++ b/sale_blanket_order/security/ir.model.access.csv @@ -0,0 +1,7 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_sale_blanket_order,sale.order,model_sale_blanket_order,sales_team.group_sale_salesman,1,1,1,0 +access_sale_blanket_order_line,sale.order.line,model_sale_blanket_order_line,sales_team.group_sale_salesman,1,1,1,1 +access_sale_blanket_order_manager,sale.order.manager,model_sale_blanket_order,sales_team.group_sale_manager,1,1,1,1 +access_sale_blanket_order_line_manager,sale.order.line.manager,model_sale_blanket_order_line,sales_team.group_sale_manager,1,1,1,1 +access_sale_blanket_order_accountant,sale.order.accountant,model_sale_blanket_order,account.group_account_user,1,1,0,0 +access_sale_blanket_order_line_accountant,sale.order.line accountant,model_sale_blanket_order_line,account.group_account_user,1,1,0,0 diff --git a/sale_blanket_order/security/security.xml b/sale_blanket_order/security/security.xml new file mode 100644 index 00000000000..b09fd5f1057 --- /dev/null +++ b/sale_blanket_order/security/security.xml @@ -0,0 +1,19 @@ + + + + + + Blanket Order multi-company + + + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] + + + + Blanket Order Line multi-company + + + ['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])] + + + diff --git a/sale_blanket_order/tests/__init__.py b/sale_blanket_order/tests/__init__.py new file mode 100644 index 00000000000..1a8245a5ce3 --- /dev/null +++ b/sale_blanket_order/tests/__init__.py @@ -0,0 +1 @@ +from . import test_blanket_orders diff --git a/sale_blanket_order/tests/test_blanket_orders.py b/sale_blanket_order/tests/test_blanket_orders.py new file mode 100644 index 00000000000..cd69e9f00da --- /dev/null +++ b/sale_blanket_order/tests/test_blanket_orders.py @@ -0,0 +1,85 @@ +# coding: utf-8 +# Copyright 2018 Acsone +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from datetime import date, timedelta + +from odoo.tests import common +from odoo import fields +from odoo.exceptions import Warning + + +class TestBlanketOrders(common.TransactionCase): + + def test_create_sale_orders(self): + partner = self.env['res.partner'].create({ + 'name': 'TEST', + 'customer': True, + }) + payment_term = self.env.ref('account.account_payment_term_net') + product = self.env['product.product'].create({ + 'name': 'Demo', + 'categ_id': self.env.ref('product.product_category_1').id, + 'standard_price': 35.0, + 'list_price': 40.0, + 'type': 'consu', + 'uom_id': self.env.ref('product.product_uom_unit').id, + 'default_code': 'PROD_DEL02', + }) + sale_pricelist = self.env['product.pricelist'].create({ + 'name': 'Sale pricelist', + 'discount_policy': 'without_discount', + 'item_ids': [(0, 0, { + 'compute_price': 'fixed', + 'fixed_price': 56.0, + 'product_id': product.id, + 'applied_on': '0_product_variant', + })] + }) + yesterday = date.today() - timedelta(days=1) + tomorrow = date.today() + timedelta(days=1) + + blanket_order = self.env['sale.blanket.order'].create({ + 'partner_id': partner.id, + 'validity_date': fields.Date.to_string(yesterday), + 'payment_term_id': payment_term.id, + 'pricelist_id': sale_pricelist.id, + 'lines_ids': [(0, 0, { + 'product_id': product.id, + 'product_uom': product.uom_id.id, + 'original_qty': 20.0, + 'price_unit': 1.0, # will be updated by pricelist + })], + }) + blanket_order.onchange_partner_id() + blanket_order.pricelist_id = sale_pricelist + blanket_order.lines_ids[0].onchange_product() + + self.assertEqual(blanket_order.state, 'draft') + self.assertEqual(blanket_order.lines_ids[0].price_unit, 56.0) + + # date in the past + with self.assertRaises(Warning): + blanket_order.action_confirm() + + blanket_order.validity_date = fields.Date.to_string(tomorrow) + blanket_order.action_confirm() + + self.assertEqual(blanket_order.state, 'opened') + + wizard1 = self.env['sale.blanket.order.wizard'].with_context( + active_id=blanket_order.id).create({}) + wizard1.lines_ids[0].write({'qty': 10.0}) + wizard1.create_sale_order() + + wizard2 = self.env['sale.blanket.order.wizard'].with_context( + active_id=blanket_order.id).create({}) + wizard2.lines_ids[0].write({'qty': 10.0}) + wizard2.create_sale_order() + + self.assertEqual(blanket_order.state, 'expired') + + self.assertEqual(blanket_order.sale_count, 2) + + view_action = blanket_order.action_view_sale_orders() + domain_ids = view_action['domain'][0][2] + self.assertEqual(len(domain_ids), 2) diff --git a/sale_blanket_order/views/blanket_orders.xml b/sale_blanket_order/views/blanket_orders.xml new file mode 100644 index 00000000000..1b28e745c9e --- /dev/null +++ b/sale_blanket_order/views/blanket_orders.xml @@ -0,0 +1,122 @@ + + + + sale.blanket.order.tree + sale.blanket.order + + + + + + + + + + + + sale.blanket.order.form + sale.blanket.order + +
+
+
+ +
+ +
+
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + +
+ + +
+ + + + + + sale.blanket.order.search + sale.blanket.order + + + + + + + + + Blanket Orders + ir.actions.act_window + sale.blanket.order + form + tree,form + + [] + {} + + + + diff --git a/sale_blanket_order/views/sale_orders.xml b/sale_blanket_order/views/sale_orders.xml new file mode 100644 index 00000000000..c2f03c4d901 --- /dev/null +++ b/sale_blanket_order/views/sale_orders.xml @@ -0,0 +1,50 @@ + + + + sale.order.from.blanket.form + sale.order + + + + + + + + + + + + {'readonly': ['|', '|', ('qty_invoiced', '>', 0), ('procurement_ids', '!=', []), ('blanket_line_id', '!=', False)]} + + + {'readonly': [('blanket_line_id', '!=', False)]} + + + {'readonly': ['|', ('state', 'in', ('sale','done', 'cancel')), ('blanket_line_id', '!=', False)]} + + + {'readonly': [('blanket_line_id', '!=', False)]} + + + + + blanket_order_id==False + + + + + + {'readonly': ['|', '|', ('qty_invoiced', '>', 0), ('procurement_ids', '!=', []), ('blanket_line_id', '!=', False)]} + + + {'readonly': [('blanket_line_id', '!=', False)]} + + + {'readonly': ['|', ('state', 'in', ('sale','done', 'cancel')), ('blanket_line_id', '!=', False)]} + + + {'readonly': [('blanket_line_id', '!=', False)]} + + + + diff --git a/sale_blanket_order/wizard/__init__.py b/sale_blanket_order/wizard/__init__.py new file mode 100644 index 00000000000..76c9168fea4 --- /dev/null +++ b/sale_blanket_order/wizard/__init__.py @@ -0,0 +1 @@ +from . import create_sale_orders diff --git a/sale_blanket_order/wizard/create_sale_orders.py b/sale_blanket_order/wizard/create_sale_orders.py new file mode 100644 index 00000000000..7eff7b7ee18 --- /dev/null +++ b/sale_blanket_order/wizard/create_sale_orders.py @@ -0,0 +1,111 @@ +# coding: utf-8 +# Copyright 2018 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import fields, models, api, _ +from odoo.exceptions import Warning + + +class BlanketOrderWizard(models.TransientModel): + _name = 'sale.blanket.order.wizard' + _description = 'Blanket Order Wizard' + + @api.model + def _default_order(self): + # in case the cron hasn't run + self.env['sale.blanket.order'].expire_orders() + if not self.env.context.get('active_id'): + return False + blanket_order = self.env['sale.blanket.order'].browse( + self.env.context['active_id']) + if blanket_order.state == 'expired': + raise Warning(_('You can\'t create a sale order from ' + 'an expired blanket order!')) + return blanket_order + + @api.model + def _default_lines(self): + blanket_order = self._default_order() + lines = [(0, 0, { + 'blanket_line_id': l.id, + 'product_id': l.product_id.id, + 'remaining_qty': l.remaining_qty, + 'qty': l.remaining_qty, + }) for l in blanket_order.lines_ids] + return lines + + blanket_order_id = fields.Many2one( + 'sale.blanket.order', default=_default_order, readonly=True) + lines_ids = fields.One2many( + 'sale.blanket.order.wizard.line', 'wizard_id', + string='Lines', default=_default_lines) + + @api.multi + def create_sale_order(self): + self.ensure_one() + + line_obj = self.env['sale.order.line'] + order_lines = [] + for line in self.lines_ids: + if line.qty == 0.0: + continue + + if line.qty > line.remaining_qty: + raise Warning( + _('You can\'t order more than the remaining quantities')) + + vals = { + 'product_id': line.product_id.id, + 'product_uom': line.blanket_line_id.product_uom.id, + 'sequence': line.blanket_line_id.sequence, + 'price_unit': line.blanket_line_id.price_unit, + 'blanket_line_id': line.blanket_line_id.id, + } + vals.update(line_obj.onchange( + vals, 'product_id', {'product_id': 'true'})['value']) + vals['product_uom_qty'] = line.qty + order_lines.append((0, 0, vals)) + + if not order_lines: + raise Warning(_('An order can\'t be empty')) + + order_vals = { + 'partner_id': self.blanket_order_id.partner_id.id, + } + order_vals.update(self.env['sale.order'].onchange( + order_vals, 'partner_id', {'partner_id': 'true'})['value']) + order_vals.update({ + 'user_id': self.blanket_order_id.user_id.id, + 'origin': self.blanket_order_id.name, + 'currency_id': self.blanket_order_id.currency_id.id, + 'order_line': order_lines, + 'pricelist_id': (self.blanket_order_id.pricelist_id.id + if self.blanket_order_id.pricelist_id + else False), + 'payment_term_id': (self.blanket_order_id.payment_term_id.id + if self.blanket_order_id.payment_term_id + else False), + }) + + sale_order = self.env['sale.order'].create(order_vals) + return { + 'type': 'ir.actions.act_window', + 'view_type': 'form', + 'view_mode': 'form', + 'res_model': 'sale.order', + 'res_id': sale_order.id, + } + + +class BlanketOrderWizardLine(models.TransientModel): + _name = 'sale.blanket.order.wizard.line' + + wizard_id = fields.Many2one('sale.blanket.order.wizard') + blanket_line_id = fields.Many2one( + 'sale.blanket.order.line') + product_id = fields.Many2one( + 'product.product', + related='blanket_line_id.product_id', + string='Product', readonly=True) + remaining_qty = fields.Float( + related='blanket_line_id.remaining_qty', readonly=True) + qty = fields.Float(string='Quantity to Order', required=True) diff --git a/sale_blanket_order/wizard/create_sale_orders.xml b/sale_blanket_order/wizard/create_sale_orders.xml new file mode 100644 index 00000000000..95d5bec206d --- /dev/null +++ b/sale_blanket_order/wizard/create_sale_orders.xml @@ -0,0 +1,34 @@ + + + + Create Sale Order + sale.blanket.order.wizard + +
+ + + + + + + + + +
+
+
+
+
+ + + Create Sale Order + ir.actions.act_window + sale.blanket.order.wizard + form + form + new + +
From 2fa1f8747d524e7e2fe31029e8a9623aa4d0856d Mon Sep 17 00:00:00 2001 From: andreparames Date: Tue, 13 Feb 2018 15:23:05 +0100 Subject: [PATCH 02/57] sale_blanket_order: don't copy name nor confirm state When duplicating a confirmed blanket order, the new copy shouldn't keep the state nor the sequence number (name). --- sale_blanket_order/models/blanket_orders.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/sale_blanket_order/models/blanket_orders.py b/sale_blanket_order/models/blanket_orders.py index 284345e91bf..fc1870b17b7 100644 --- a/sale_blanket_order/models/blanket_orders.py +++ b/sale_blanket_order/models/blanket_orders.py @@ -31,7 +31,8 @@ def _default_company(self): 'res.partner', string='Partner', readonly=True, states={'draft': [('readonly', False)]}) lines_ids = fields.One2many( - 'sale.blanket.order.line', 'order_id', string='Order lines') + 'sale.blanket.order.line', 'order_id', string='Order lines', + copy=True) pricelist_id = fields.Many2one( 'product.pricelist', string='Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)]}) @@ -40,7 +41,7 @@ def _default_company(self): payment_term_id = fields.Many2one( 'account.payment.term', string='Payment Terms', readonly=True, states={'draft': [('readonly', False)]}) - confirmed = fields.Boolean() + confirmed = fields.Boolean(default=False) state = fields.Selection(selection=[ ('draft', 'Draft'), ('opened', 'Opened'), @@ -130,6 +131,13 @@ def onchange_partner_id(self): values['team_id'] = self.partner_id.team_id.id self.update(values) + @api.multi + def copy_data(self, default=None): + if default is None: + default = {} + default.update(self.default_get(['name', 'confirmed'])) + return super(BlanketOrder, self).copy_data(default) + @api.multi def _validate(self): try: From 2804567c6769c49db9b695721add2a79af9a3bae Mon Sep 17 00:00:00 2001 From: andreparames Date: Tue, 27 Mar 2018 14:48:17 +0200 Subject: [PATCH 03/57] sale_blanket_order: update after review --- sale_blanket_order/models/blanket_orders.py | 24 +++++++++---------- sale_blanket_order/models/sale_orders.py | 3 ++- .../wizard/create_sale_orders.py | 10 ++++---- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/sale_blanket_order/models/blanket_orders.py b/sale_blanket_order/models/blanket_orders.py index fc1870b17b7..a5dd5798fd7 100644 --- a/sale_blanket_order/models/blanket_orders.py +++ b/sale_blanket_order/models/blanket_orders.py @@ -2,9 +2,11 @@ # Copyright 2018 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo import fields, models, api, _ -from odoo.exceptions import Warning +from odoo.exceptions import UserError from odoo.tools import float_is_zero +import odoo.addons.decimal_precision as dp + class BlanketOrder(models.Model): _name = 'sale.blanket.order' @@ -41,10 +43,10 @@ def _default_company(self): payment_term_id = fields.Many2one( 'account.payment.term', string='Payment Terms', readonly=True, states={'draft': [('readonly', False)]}) - confirmed = fields.Boolean(default=False) + confirmed = fields.Boolean() state = fields.Selection(selection=[ ('draft', 'Draft'), - ('opened', 'Opened'), + ('opened', 'Open'), ('expired', 'Expired'), ], compute='_compute_state', store=True) validity_date = fields.Date( @@ -71,11 +73,7 @@ def _default_company(self): @api.multi def _get_sale_orders(self): - lines = self.mapped('lines_ids') - sale_lines = lines.mapped('sale_order_lines_ids') - sale_orders = sale_lines.mapped('order_id') - sale_orders_ids = list(set(sale_orders.ids)) - return self.env['sale.order'].browse(sale_orders_ids) + return self.mapped('lines_ids.sale_order_lines_ids.order_id') @api.multi @api.depends('lines_ids.remaining_qty') @@ -150,7 +148,7 @@ def _validate(self): assert len(order.lines_ids) > 0, _("Must have some lines") order.lines_ids._validate() except AssertionError as e: - raise Warning(e.message) + raise UserError(e.message) @api.multi def action_confirm(self): @@ -197,7 +195,8 @@ class BlanketOrderLine(models.Model): 'product.uom', string='Unit of Measure', required=True) price_unit = fields.Float(string='Price', required=True) original_qty = fields.Float( - string='Original quantity', required=True, default=1) + string='Original quantity', required=True, default=1, + digits=dp.get_precision('Product Unit of Measure')) ordered_qty = fields.Float( string='Ordered quantity', compute='_compute_quantities', store=True) invoiced_qty = fields.Float( @@ -209,7 +208,8 @@ class BlanketOrderLine(models.Model): sale_order_lines_ids = fields.One2many( 'sale.order.line', 'blanket_line_id', string='Sale order lines') company_id = fields.Many2one( - 'res.company', related='order_id.company_id', store=True) + 'res.company', related='order_id.company_id', store=True, + readonly=True) def _get_real_price_currency(self, product, rule_id, qty, uom, pricelist_id): @@ -326,4 +326,4 @@ def _validate(self): assert line.original_qty > 0.0, \ _("Quantity must be greater than zero") except AssertionError as e: - raise Warning(e.message) + raise UserError(e.message) diff --git a/sale_blanket_order/models/sale_orders.py b/sale_blanket_order/models/sale_orders.py index 448b4d69ad7..0fb0877cba7 100644 --- a/sale_blanket_order/models/sale_orders.py +++ b/sale_blanket_order/models/sale_orders.py @@ -9,7 +9,8 @@ class SaleOrder(models.Model): blanket_order_id = fields.Many2one( 'sale.blanket.order', string='Origin blanket order', - related='order_line.blanket_line_id.order_id') + related='order_line.blanket_line_id.order_id', + readonly=True) class SaleOrderLine(models.Model): diff --git a/sale_blanket_order/wizard/create_sale_orders.py b/sale_blanket_order/wizard/create_sale_orders.py index 7eff7b7ee18..42ce60d7cc6 100644 --- a/sale_blanket_order/wizard/create_sale_orders.py +++ b/sale_blanket_order/wizard/create_sale_orders.py @@ -2,7 +2,7 @@ # Copyright 2018 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from odoo import fields, models, api, _ -from odoo.exceptions import Warning +from odoo.exceptions import UserError class BlanketOrderWizard(models.TransientModel): @@ -18,8 +18,8 @@ def _default_order(self): blanket_order = self.env['sale.blanket.order'].browse( self.env.context['active_id']) if blanket_order.state == 'expired': - raise Warning(_('You can\'t create a sale order from ' - 'an expired blanket order!')) + raise UserError(_('You can\'t create a sale order from ' + 'an expired blanket order!')) return blanket_order @api.model @@ -50,7 +50,7 @@ def create_sale_order(self): continue if line.qty > line.remaining_qty: - raise Warning( + raise UserError( _('You can\'t order more than the remaining quantities')) vals = { @@ -66,7 +66,7 @@ def create_sale_order(self): order_lines.append((0, 0, vals)) if not order_lines: - raise Warning(_('An order can\'t be empty')) + raise UserError(_('An order can\'t be empty')) order_vals = { 'partner_id': self.blanket_order_id.partner_id.id, From 92890c51d043ec96bc47588399983ad7463ee419 Mon Sep 17 00:00:00 2001 From: andreparames Date: Fri, 30 Mar 2018 10:23:49 +0200 Subject: [PATCH 04/57] sale_blanket_order: Re-calculate price based on qty --- sale_blanket_order/models/blanket_orders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sale_blanket_order/models/blanket_orders.py b/sale_blanket_order/models/blanket_orders.py index a5dd5798fd7..1a04082ee46 100644 --- a/sale_blanket_order/models/blanket_orders.py +++ b/sale_blanket_order/models/blanket_orders.py @@ -294,7 +294,7 @@ def _get_display_price(self, product): return max(base_price, final_price) @api.multi - @api.onchange('product_id') + @api.onchange('product_id', 'original_qty') def onchange_product(self): if self.product_id: self.product_uom = self.product_id.uom_id.id From 86eb1bfef16654c35d05fe4a46a985537cf56eb0 Mon Sep 17 00:00:00 2001 From: andreparames Date: Thu, 26 Apr 2018 15:58:28 +0200 Subject: [PATCH 05/57] sale_blanket_order: make prohibition of adding new lines optional --- sale_blanket_order/__manifest__.py | 1 + sale_blanket_order/models/__init__.py | 1 + sale_blanket_order/models/blanket_orders.py | 1 + .../models/sale_config_settings.py | 14 ++++++++++++ sale_blanket_order/security/security.xml | 4 ++++ .../tests/test_blanket_orders.py | 4 ++-- .../views/sale_config_settings.xml | 22 +++++++++++++++++++ sale_blanket_order/views/sale_orders.xml | 15 ++++++++++--- 8 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 sale_blanket_order/models/sale_config_settings.py create mode 100644 sale_blanket_order/views/sale_config_settings.xml diff --git a/sale_blanket_order/__manifest__.py b/sale_blanket_order/__manifest__.py index 7579b7177f5..89fc4472946 100644 --- a/sale_blanket_order/__manifest__.py +++ b/sale_blanket_order/__manifest__.py @@ -14,6 +14,7 @@ 'web_action_conditionable', ], 'data': [ + 'views/sale_config_settings.xml', 'data/sequence.xml', 'data/ir_cron.xml', 'security/ir.model.access.csv', diff --git a/sale_blanket_order/models/__init__.py b/sale_blanket_order/models/__init__.py index 36b4289c0ec..24f15a33dbd 100644 --- a/sale_blanket_order/models/__init__.py +++ b/sale_blanket_order/models/__init__.py @@ -1,2 +1,3 @@ from . import blanket_orders from . import sale_orders +from . import sale_config_settings diff --git a/sale_blanket_order/models/blanket_orders.py b/sale_blanket_order/models/blanket_orders.py index 1a04082ee46..3de7c14cedf 100644 --- a/sale_blanket_order/models/blanket_orders.py +++ b/sale_blanket_order/models/blanket_orders.py @@ -274,6 +274,7 @@ def _get_real_price_currency(self, product, rule_id, qty, uom, @api.multi def _get_display_price(self, product): # Copied and adapted from the sale module + self.ensure_one() pricelist = self.order_id.pricelist_id partner = self.order_id.partner_id if self.order_id.pricelist_id.discount_policy == 'with_discount': diff --git a/sale_blanket_order/models/sale_config_settings.py b/sale_blanket_order/models/sale_config_settings.py new file mode 100644 index 00000000000..5853a195441 --- /dev/null +++ b/sale_blanket_order/models/sale_config_settings.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +# Copyright 2018 ACSONE SA/NV +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class SaleConfigSettings(models.TransientModel): + + _inherit = 'sale.config.settings' + + group_blanket_disable_adding_lines = fields.Boolean( + string='Disable adding more lines to SOs', + implied_group='sale_blanket_order.blanket_orders_disable_adding_lines') diff --git a/sale_blanket_order/security/security.xml b/sale_blanket_order/security/security.xml index b09fd5f1057..63844007b4b 100644 --- a/sale_blanket_order/security/security.xml +++ b/sale_blanket_order/security/security.xml @@ -1,5 +1,9 @@ + + Disable adding more lines to SOs from Blanket Orders + + diff --git a/sale_blanket_order/tests/test_blanket_orders.py b/sale_blanket_order/tests/test_blanket_orders.py index cd69e9f00da..4219547ab8e 100644 --- a/sale_blanket_order/tests/test_blanket_orders.py +++ b/sale_blanket_order/tests/test_blanket_orders.py @@ -5,7 +5,7 @@ from odoo.tests import common from odoo import fields -from odoo.exceptions import Warning +from odoo.exceptions import UserError class TestBlanketOrders(common.TransactionCase): @@ -58,7 +58,7 @@ def test_create_sale_orders(self): self.assertEqual(blanket_order.lines_ids[0].price_unit, 56.0) # date in the past - with self.assertRaises(Warning): + with self.assertRaises(UserError): blanket_order.action_confirm() blanket_order.validity_date = fields.Date.to_string(tomorrow) diff --git a/sale_blanket_order/views/sale_config_settings.xml b/sale_blanket_order/views/sale_config_settings.xml new file mode 100644 index 00000000000..fda2e4381d2 --- /dev/null +++ b/sale_blanket_order/views/sale_config_settings.xml @@ -0,0 +1,22 @@ + + + + + + + sale.config.settings.form (in sale_blanket_order) + sale.config.settings + + + + + + + + + + + + + diff --git a/sale_blanket_order/views/sale_orders.xml b/sale_blanket_order/views/sale_orders.xml index c2f03c4d901..7ace038623d 100644 --- a/sale_blanket_order/views/sale_orders.xml +++ b/sale_blanket_order/views/sale_orders.xml @@ -27,9 +27,6 @@ - - blanket_order_id==False - @@ -47,4 +44,16 @@ + + + sale.order.from.blanket.form - disable adding lines + sale.order + + + + + blanket_order_id==False + + + From 1a6b12e0536ff8a70a51d113780ea02b2d28c851 Mon Sep 17 00:00:00 2001 From: Denis Roussel Date: Tue, 4 Sep 2018 10:21:05 +0200 Subject: [PATCH 06/57] [FIX] sale_blanket_order: Ondelete cascade on blanket lines --- sale_blanket_order/models/blanket_orders.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sale_blanket_order/models/blanket_orders.py b/sale_blanket_order/models/blanket_orders.py index 3de7c14cedf..c84ff679ba5 100644 --- a/sale_blanket_order/models/blanket_orders.py +++ b/sale_blanket_order/models/blanket_orders.py @@ -188,7 +188,8 @@ class BlanketOrderLine(models.Model): _description = 'Blanket Order Line' sequence = fields.Integer() - order_id = fields.Many2one('sale.blanket.order', required=True) + order_id = fields.Many2one( + 'sale.blanket.order', required=True, ondelete='cascade') product_id = fields.Many2one( 'product.product', string='Product', required=True) product_uom = fields.Many2one( From a9bfbf2a7d98adf7678032bb6da9b3606629afc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Gil=20Sorribes?= Date: Thu, 16 May 2019 09:51:16 +0200 Subject: [PATCH 07/57] [11.0][MIG] Migrate module sale_blanket_order --- sale_blanket_order/README.rst | 132 +++- sale_blanket_order/__init__.py | 2 + sale_blanket_order/__manifest__.py | 17 +- sale_blanket_order/data/ir_cron.xml | 11 +- .../i18n/sale_blanket_order.pot | 714 ++++++++++++++++++ sale_blanket_order/models/blanket_orders.py | 371 +++++++-- .../models/sale_config_settings.py | 3 +- sale_blanket_order/models/sale_orders.py | 139 +++- sale_blanket_order/readme/CONTRIBUTORS.rst | 3 + sale_blanket_order/readme/DESCRIPTION.rst | 4 + sale_blanket_order/readme/USAGE.rst | 53 ++ sale_blanket_order/report/templates.xml | 51 +- .../security/ir.model.access.csv | 2 + .../static/description/BO_actions.png | Bin 0 -> 41353 bytes .../static/description/BO_form.png | Bin 0 -> 38832 bytes .../static/description/BO_lines.png | Bin 0 -> 27469 bytes .../static/description/BO_menu.png | Bin 0 -> 23846 bytes .../static/description/PO_BOLine.png | Bin 0 -> 37234 bytes .../static/description/PO_from_BO.png | Bin 0 -> 27671 bytes .../static/description/icon.png | Bin 0 -> 9455 bytes .../static/description/index.html | 471 ++++++++++++ sale_blanket_order/tests/__init__.py | 3 + .../tests/test_blanket_orders.py | 275 +++++-- sale_blanket_order/tests/test_sale_order.py | 151 ++++ sale_blanket_order/views/blanket_orders.xml | 122 --- .../views/sale_blanket_order_views.xml | 313 ++++++++ .../views/sale_config_settings.xml | 25 +- sale_blanket_order/views/sale_order_views.xml | 28 + sale_blanket_order/views/sale_orders.xml | 59 -- sale_blanket_order/wizard/__init__.py | 2 + .../wizard/create_sale_orders.py | 193 +++-- .../wizard/create_sale_orders.xml | 9 +- 32 files changed, 2756 insertions(+), 397 deletions(-) create mode 100644 sale_blanket_order/i18n/sale_blanket_order.pot create mode 100644 sale_blanket_order/readme/CONTRIBUTORS.rst create mode 100644 sale_blanket_order/readme/DESCRIPTION.rst create mode 100644 sale_blanket_order/readme/USAGE.rst create mode 100644 sale_blanket_order/static/description/BO_actions.png create mode 100644 sale_blanket_order/static/description/BO_form.png create mode 100644 sale_blanket_order/static/description/BO_lines.png create mode 100644 sale_blanket_order/static/description/BO_menu.png create mode 100644 sale_blanket_order/static/description/PO_BOLine.png create mode 100644 sale_blanket_order/static/description/PO_from_BO.png create mode 100644 sale_blanket_order/static/description/icon.png create mode 100644 sale_blanket_order/static/description/index.html create mode 100644 sale_blanket_order/tests/test_sale_order.py delete mode 100644 sale_blanket_order/views/blanket_orders.xml create mode 100644 sale_blanket_order/views/sale_blanket_order_views.xml create mode 100644 sale_blanket_order/views/sale_order_views.xml delete mode 100644 sale_blanket_order/views/sale_orders.xml diff --git a/sale_blanket_order/README.rst b/sale_blanket_order/README.rst index a8228489828..e0f76f1b7bb 100644 --- a/sale_blanket_order/README.rst +++ b/sale_blanket_order/README.rst @@ -1,69 +1,135 @@ -.. image:: https://img.shields.io/badge/license-AGPL--3-blue.png - :target: https://www.gnu.org/licenses/agpl - :alt: License: AGPL-3 - -============== -Blanket Orders -============== +=================== +Sale Blanket Orders +=================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fsale--workflow-lightgray.png?logo=github + :target: https://github.com/OCA/sale-workflow/tree/11.0/sale_blanket_order + :alt: OCA/sale-workflow +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/sale-workflow-11-0/sale-workflow-11-0-sale_blanket_order + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/167/11.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| A blanket order is a pre-agreement to sell a certain number of quantities of products at a specific price. From a confirmed blanket order, the users can create new sale orders at such price, until the blanket order expires, either due to reaching the validity date or exhausting all the quantities of products. +**Table of contents** + +.. contents:: + :local: + Usage ===== -A new menu in the Sale area is created, allowing users to create new blanket -orders. +A new menu in the Sales area is created, allowing users to create new blanket orders. + +To create a new Sale Blanket Order go to the sale menu in the Sales section: + +.. image:: https://raw.githubusercontent.com/sale_blanket_order/static/description/BO_menu.png + :alt: Blanket Orders menu + +Hitting the button create will open the form view in which we can introduce the following +information: + +* Vendor +* Salesperson +* Payment Terms +* Validity date +* Order lines: + * Product + * Accorded price + * Original, Ordered, Invoiced, Received and Remaining quantities +* Terms and Conditions of the Blanket Order + +.. image:: https://raw.githubusercontent.com/sale_blanket_order/static/description/BO_form.png + :alt: Blanket Orders form + +From the form, once the Blanket Order has been confirmed and its state is open, the user can +create a Sale Order, check the Sale Orders associated to the Blanket Order and/or +see the Blanket Order lines associated to the BO. + +.. image:: https://raw.githubusercontent.com/sale_blanket_order/static/description/BO_actions.png + :alt: Actions that can be done from Blanket Order + +Hitting the button Create Sale Order will open a wizard that will ask for the amount of each +product in the BO lines for which the Sale Order will be created. + +.. image:: https://raw.githubusercontent.com/sale_blanket_order/static/description/PO_from_BO.png + :alt: Create Sale Order from Blanket Order + +Installing this module will add an additional menu which will show all the blanket order lines +currently defined in the system. From this list the user can create customized Sale Orders +selecting the lines for which the PO (or POs if the customers are different) is (are) created. -.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas - :alt: Try me on Runbot - :target: https://runbot.odoo-community.org/runbot/167/11.0 +.. image:: https://raw.githubusercontent.com/sale_blanket_order/static/description/BO_lines.png + :alt: Blanket Order lines and actions +In the Sale Order form one field is added in the PO lines, the Blanket Order line field. This +field keeps track to which Blanket Order line the PO line is associated. Upon adding a new product +in a newly created Sale Order a blanket order line will be suggested depending on the following +factors: +* Closer Validity date +* Remaining quantity > Quantity introduced in the Sale Order line + +.. image:: https://raw.githubusercontent.com/sale_blanket_order/static/description/PO_BOLine.png + :alt: New field added in Sale Order Line Bug Tracker =========== -Bugs are tracked on `GitHub Issues -`_. In case of trouble, please -check there if your issue has already been reported. If you spotted it first, -help us smash it by providing detailed and welcomed feedback. +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. Credits ======= -Images ------- +Authors +~~~~~~~ -* Odoo Community Association: `Icon `_. +* Acsone SA/NV Contributors ------------- +~~~~~~~~~~~~ * André Pereira (https://www.acsone.eu/) +* Adrià Gil Sorribes (https://www.eficent.com/) +* Jordi Ballester Alomar -Do not contact contributors directly about support or help with technical issues. - -Funders -------- - -The development of this module has been financially supported by: +Maintainers +~~~~~~~~~~~ -* Acsone SA/NV - -Maintainer ----------- +This module is maintained by the OCA. .. image:: https://odoo-community.org/logo.png :alt: Odoo Community Association :target: https://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 https://odoo-community.org. +This module is part of the `OCA/sale-workflow `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/sale_blanket_order/__init__.py b/sale_blanket_order/__init__.py index 9b4296142f4..93aa2c1f84b 100644 --- a/sale_blanket_order/__init__.py +++ b/sale_blanket_order/__init__.py @@ -1,2 +1,4 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + from . import models from . import wizard diff --git a/sale_blanket_order/__manifest__.py b/sale_blanket_order/__manifest__.py index 89fc4472946..b40cf51e9c8 100644 --- a/sale_blanket_order/__manifest__.py +++ b/sale_blanket_order/__manifest__.py @@ -1,12 +1,11 @@ -# coding: utf-8 # Copyright 2018 Acsone # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). { - 'name': 'Blanket Orders', + 'name': 'Sale Blanket Orders', 'category': 'Sale', 'license': 'AGPL-3', - 'author': 'Acsone SA/NV,Odoo Community Association (OCA)', - 'version': '10.0.1.0.0', + 'author': 'Acsone SA/NV, Odoo Community Association (OCA)', + 'version': '11.0.1.0.0', 'website': 'https://github.com/OCA/sale-workflow', 'summary': "Blanket Orders", 'depends': [ @@ -14,14 +13,14 @@ 'web_action_conditionable', ], 'data': [ - 'views/sale_config_settings.xml', + 'security/security.xml', + 'security/ir.model.access.csv', 'data/sequence.xml', 'data/ir_cron.xml', - 'security/ir.model.access.csv', - 'security/security.xml', 'wizard/create_sale_orders.xml', - 'views/blanket_orders.xml', - 'views/sale_orders.xml', + 'views/sale_config_settings.xml', + 'views/sale_blanket_order_views.xml', + 'views/sale_order_views.xml', 'report/templates.xml', 'report/report.xml', ], diff --git a/sale_blanket_order/data/ir_cron.xml b/sale_blanket_order/data/ir_cron.xml index cd6453937cd..2606240edee 100644 --- a/sale_blanket_order/data/ir_cron.xml +++ b/sale_blanket_order/data/ir_cron.xml @@ -4,17 +4,16 @@ - + Expire Blanket Orders 1 days - + -1 - sale.blanket.order - expire_orders - () + + code + model.expire_orders() diff --git a/sale_blanket_order/i18n/sale_blanket_order.pot b/sale_blanket_order/i18n/sale_blanket_order.pot new file mode 100644 index 00000000000..21f791bf390 --- /dev/null +++ b/sale_blanket_order/i18n/sale_blanket_order.pot @@ -0,0 +1,714 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * sale_blanket_order +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \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_blanket_order +#: model:ir.ui.view,arch_db:sale_blanket_order.report_blanketorder_document +msgid "Blanket Order # " +msgstr "" + +#. module: sale_blanket_order +#: model:ir.ui.view,arch_db:sale_blanket_order.report_blanketorder_document +msgid "Currency:" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.ui.view,arch_db:sale_blanket_order.report_blanketorder_document +msgid "Salesperson:" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.ui.view,arch_db:sale_blanket_order.report_blanketorder_document +msgid "Subtotal" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.ui.view,arch_db:sale_blanket_order.report_blanketorder_document +msgid "Total" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.ui.view,arch_db:sale_blanket_order.report_blanketorder_document +msgid "Validity Date:" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.ui.view,arch_db:sale_blanket_order.report_blanketorder_document +msgid "Your Reference:" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.ui.view,arch_db:sale_blanket_order.report_blanketorder_document +msgid "Amount" +msgstr "" + +#. module: sale_blanket_order +#: code:addons/sale_blanket_order/wizard/create_sale_orders.py:135 +#, python-format +msgid "An order can't be empty" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_wizard_line_blanket_line_id +msgid "Blanket Line" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.actions.report,name:sale_blanket_order.report_blanket_order +#: model:ir.model,name:sale_blanket_order.model_sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_wizard_blanket_order_id +#: model:ir.ui.view,arch_db:sale_blanket_order.view_blanket_order_form +msgid "Blanket Order" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model,name:sale_blanket_order.model_sale_blanket_order_line +msgid "Blanket Order Line" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.actions.act_window,name:sale_blanket_order.act_open_sale_blanket_order_lines_view_tree +#: model:ir.ui.menu,name:sale_blanket_order.menu_sale_blanket_order_line +msgid "Blanket Order Lines" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model,name:sale_blanket_order.model_sale_blanket_order_wizard +msgid "Blanket Order Wizard" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_order_line_blanket_order_line +msgid "Blanket Order line" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.actions.act_window,name:sale_blanket_order.act_open_blanket_order_view +#: model:ir.ui.menu,name:sale_blanket_order.menu_blanket_order_config +#: model:ir.ui.view,arch_db:sale_blanket_order.sale_config_settings_form_view +msgid "Blanket Orders" +msgstr "" + +#. module: sale_blanket_order +#: code:addons/sale_blanket_order/wizard/create_sale_orders.py:138 +#, python-format +msgid "Can not create Sale Order from Blanket Order lines with different currencies" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.ui.view,arch_db:sale_blanket_order.view_blanket_order_form +#: model:ir.ui.view,arch_db:sale_blanket_order.view_create_sale_order +msgid "Cancel" +msgstr "" + +#. module: sale_blanket_order +#: code:addons/sale_blanket_order/models/sale_orders.py:28 +#, python-format +msgid "Cannot confirm order %s as one of the lines refers to a blanket order that has no remaining quantity." +msgstr "" + +#. module: sale_blanket_order +#: model:ir.actions.act_window,help:sale_blanket_order.act_open_blanket_order_view +msgid "Click to create a blanket order that can be converted into a sale order." +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_company_id +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_line_company_id +msgid "Company" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.ui.view,arch_db:sale_blanket_order.view_blanket_order_form +msgid "Confirm" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_confirmed +msgid "Confirmed" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.actions.act_window,name:sale_blanket_order.action_create_sale_order +#: model:ir.ui.view,arch_db:sale_blanket_order.view_blanket_order_form +#: model:ir.ui.view,arch_db:sale_blanket_order.view_create_sale_order +msgid "Create Sale Order" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.ui.view,arch_db:sale_blanket_order.view_create_sale_order +msgid "Create and View Order" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_create_uid +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_line_create_uid +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_wizard_create_uid +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_wizard_line_create_uid +msgid "Created by" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_create_date +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_line_create_date +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_wizard_create_date +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_wizard_line_create_date +msgid "Created on" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_currency_id +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_line_currency_id +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_wizard_line_currency_id +msgid "Currency" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_line_partner_id +msgid "Customer" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_client_order_ref +msgid "Customer Reference" +msgstr "" + +#. module: sale_blanket_order +#: code:addons/sale_blanket_order/models/blanket_orders.py:445 +#, python-format +msgid "Date Scheduled" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.ui.view,arch_db:sale_blanket_order.view_blanket_order_search +msgid "Delivered Qty" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_delivered_uom_qty +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_line_delivered_uom_qty +msgid "Delivered quantity" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_line_name +msgid "Description" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_res_config_settings_group_blanket_disable_adding_lines +msgid "Disable adding more lines to SOs" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.ui.view,arch_db:sale_blanket_order.sale_config_settings_form_view +#: model:res.groups,name:sale_blanket_order.blanket_orders_disable_adding_lines +msgid "Disable adding more lines to SOs from Blanket Orders" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_display_name +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_line_display_name +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_wizard_display_name +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_wizard_line_display_name +msgid "Display Name" +msgstr "" + +#. module: sale_blanket_order +#: selection:sale.blanket.order,state:0 +msgid "Done" +msgstr "" + +#. module: sale_blanket_order +#: selection:sale.blanket.order,state:0 +msgid "Draft" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.actions.server,name:sale_blanket_order.expired_blanket_orders_cron_ir_actions_server +#: model:ir.cron,cron_name:sale_blanket_order.expired_blanket_orders_cron +#: model:ir.cron,name:sale_blanket_order.expired_blanket_orders_cron +msgid "Expire Blanket Orders" +msgstr "" + +#. module: sale_blanket_order +#: selection:sale.blanket.order,state:0 +msgid "Expired" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_fiscal_position_id +msgid "Fiscal Position" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_id +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_line_id +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_wizard_id +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_wizard_line_id +msgid "ID" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.ui.view,arch_db:sale_blanket_order.view_blanket_order_search +msgid "Invoiced Qty" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_invoiced_uom_qty +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_line_invoiced_uom_qty +msgid "Invoiced quantity" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order___last_update +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_line___last_update +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_wizard___last_update +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_wizard_line___last_update +msgid "Last Modified on" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_line_write_uid +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_wizard_line_write_uid +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_wizard_write_uid +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_write_uid +msgid "Last Updated by" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_line_write_date +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_wizard_line_write_date +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_wizard_write_date +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_write_date +msgid "Last Updated on" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_wizard_line_ids +#: model:ir.ui.view,arch_db:sale_blanket_order.view_blanket_order_form +msgid "Lines" +msgstr "" + +#. module: sale_blanket_order +#: code:addons/sale_blanket_order/models/blanket_orders.py:227 +#, python-format +msgid "Must have some lines" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_name +msgid "Name" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_note +msgid "Note" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.ui.view,arch_db:sale_blanket_order.view_blanket_order_search +#: selection:sale.blanket.order,state:0 +msgid "Open" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_line_order_id +msgid "Order" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.ui.view,arch_db:sale_blanket_order.view_blanket_order_form +msgid "Order Lines" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_line_ids +msgid "Order lines" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.ui.view,arch_db:sale_blanket_order.view_blanket_order_search +msgid "Ordered Qty" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_line_ordered_uom_qty +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_ordered_uom_qty +msgid "Ordered quantity" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_order_blanket_order_id +msgid "Origin blanket order" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.ui.view,arch_db:sale_blanket_order.report_blanketorder_document +#: model:ir.ui.view,arch_db:sale_blanket_order.sale_blanket_order_line_tree +#: model:ir.ui.view,arch_db:sale_blanket_order.view_blanket_order_form +#: model:ir.ui.view,arch_db:sale_blanket_order.view_blanket_order_search +msgid "Original Qty" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_line_original_uom_qty +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_original_uom_qty +msgid "Original quantity" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.ui.view,arch_db:sale_blanket_order.view_blanket_order_form +msgid "Other Information" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_partner_id +msgid "Partner" +msgstr "" + +#. module: sale_blanket_order +#: code:addons/sale_blanket_order/models/blanket_orders.py:226 +#, python-format +msgid "Partner is mandatory" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_line_payment_term_id +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_payment_term_id +msgid "Payment Terms" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_line_price_unit +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_wizard_line_price_unit +msgid "Price" +msgstr "" + +#. module: sale_blanket_order +#: code:addons/sale_blanket_order/models/blanket_orders.py:602 +#, python-format +msgid "Price must be greater than zero" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_line_pricelist_id +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_pricelist_id +msgid "Pricelist" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_line_product_id +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_product_id +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_wizard_line_product_id +#: model:ir.ui.view,arch_db:sale_blanket_order.report_blanketorder_document +msgid "Product" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_wizard_sale_order_id +msgid "Purchase Order" +msgstr "" + +#. module: sale_blanket_order +#: code:addons/sale_blanket_order/models/blanket_orders.py:604 +#, python-format +msgid "Quantity must be greater than zero" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_wizard_line_qty +msgid "Quantity to Order" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model,name:sale_blanket_order.model_sale_order +msgid "Quotation" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.ui.view,arch_db:sale_blanket_order.view_blanket_order_search +msgid "Remaining Qty" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_line_remaining_uom_qty +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_remaining_uom_qty +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_wizard_line_remaining_uom_qty +msgid "Remaining quantity" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_line_remaining_qty +msgid "Remaining quantity in base UoM" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_line_user_id +msgid "Responsible" +msgstr "" + +#. module: sale_blanket_order +#: code:addons/sale_blanket_order/wizard/create_sale_orders.py:36 +#, python-format +msgid "Sale Blanket Order %s is not open" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.ui.view,arch_db:sale_blanket_order.sale_blanket_order_line_form +msgid "Sale Blanket Order Line" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_line_count +msgid "Sale Blanket Order Line count" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.ui.view,arch_db:sale_blanket_order.sale_blanket_order_line_tree +msgid "Sale Blanket Order Lines" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_sale_count +msgid "Sale Count" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.ui.view,arch_db:sale_blanket_order.sale_blanket_order_line_form +msgid "Sale Order Lines" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.ui.view,arch_db:sale_blanket_order.view_blanket_order_form +msgid "Sale Orders" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_line_sale_lines +msgid "Sale order lines" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.ui.view,arch_db:sale_blanket_order.view_blanket_order_form +msgid "Sales Information" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model,name:sale_blanket_order.model_sale_order_line +msgid "Sales Order Line" +msgstr "" + +#. module: sale_blanket_order +#: code:addons/sale_blanket_order/wizard/create_sale_orders.py:163 +#, python-format +msgid "Sales Orders" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_team_id +msgid "Sales Team" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_user_id +msgid "Salesperson" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_line_date_schedule +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_wizard_line_date_schedule +#: model:ir.ui.view,arch_db:sale_blanket_order.report_blanketorder_document +msgid "Scheduled Date" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.ui.view,arch_db:sale_blanket_order.sale_blanket_order_line_search +msgid "Search Sale Blanket Order Line" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_line_sequence +msgid "Sequence" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.ui.view,arch_db:sale_blanket_order.view_blanket_order_form +msgid "Setup default terms and conditions in your company settings." +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_state +msgid "State" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_line_price_subtotal +msgid "Subtotal" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_line_price_tax +msgid "Tax" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_amount_tax +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_line_taxes_id +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_wizard_line_taxes_id +#: model:ir.ui.view,arch_db:sale_blanket_order.report_blanketorder_document +msgid "Taxes" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.ui.view,arch_db:sale_blanket_order.view_blanket_order_form +msgid "Terms and Conditions" +msgstr "" + +#. module: sale_blanket_order +#: code:addons/sale_blanket_order/models/sale_orders.py:148 +#, python-format +msgid "The currency of the blanket order must match with that of the sale order." +msgstr "" + +#. module: sale_blanket_order +#: code:addons/sale_blanket_order/models/sale_orders.py:39 +#, python-format +msgid "The customer must be equal to the blanket order lines customer" +msgstr "" + +#. module: sale_blanket_order +#: code:addons/sale_blanket_order/models/sale_orders.py:138 +#, python-format +msgid "The product in the blanket order and in the sales order must match" +msgstr "" + +#. module: sale_blanket_order +#: code:addons/sale_blanket_order/wizard/create_sale_orders.py:42 +#, python-format +msgid "The sale has already been completed." +msgstr "" + +#. module: sale_blanket_order +#: model:ir.ui.view,arch_db:sale_blanket_order.view_blanket_order_form +msgid "To Draft" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_amount_total +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_line_price_total +msgid "Total" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.ui.view,arch_db:sale_blanket_order.report_blanketorder_document +msgid "Unit Price" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_line_product_uom +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_wizard_line_product_uom +msgid "Unit of Measure" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_amount_untaxed +msgid "Untaxed Amount" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.actions.act_window,help:sale_blanket_order.act_open_blanket_order_view +msgid "Use this menu to search within your blanket orders. For each blanket order,\n" +" you can track the related discussion with the customer, control\n" +" the products delivered and control the vendor bills." +msgstr "" + +#. module: sale_blanket_order +#: model:ir.ui.view,arch_db:sale_blanket_order.report_blanketorder_document +msgid "VAT:" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_validity_date +msgid "Validity Date" +msgstr "" + +#. module: sale_blanket_order +#: code:addons/sale_blanket_order/models/blanket_orders.py:223 +#, python-format +msgid "Validity date is mandatory" +msgstr "" + +#. module: sale_blanket_order +#: code:addons/sale_blanket_order/models/blanket_orders.py:225 +#, python-format +msgid "Validity date must be in the future" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_wizard_line_partner_id +msgid "Vendor" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model.fields,field_description:sale_blanket_order.field_sale_blanket_order_wizard_line_wizard_id +msgid "Wizard" +msgstr "" + +#. module: sale_blanket_order +#: code:addons/sale_blanket_order/models/blanket_orders.py:256 +#, python-format +msgid "You can not delete a blanket order with opened purchase orders! Try to cancel them before." +msgstr "" + +#. module: sale_blanket_order +#: code:addons/sale_blanket_order/models/blanket_orders.py:206 +#, python-format +msgid "You can not delete an open blanket order! Try to cancel it before." +msgstr "" + +#. module: sale_blanket_order +#: code:addons/sale_blanket_order/wizard/create_sale_orders.py:22 +#, python-format +msgid "You can't create a sale order from an expired blanket order!" +msgstr "" + +#. module: sale_blanket_order +#: code:addons/sale_blanket_order/wizard/create_sale_orders.py:102 +#, python-format +msgid "You can't order more than the remaining quantities" +msgstr "" + +#. module: sale_blanket_order +#: code:addons/sale_blanket_order/wizard/create_sale_orders.py:48 +#, python-format +msgid "You have to select lines from the same company." +msgstr "" + +#. module: sale_blanket_order +#: code:addons/sale_blanket_order/models/blanket_orders.py:446 +#, python-format +msgid "remaining" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model,name:sale_blanket_order.model_res_config_settings +msgid "res.config.settings" +msgstr "" + +#. module: sale_blanket_order +#: model:ir.model,name:sale_blanket_order.model_sale_blanket_order_wizard_line +msgid "sale.blanket.order.wizard.line" +msgstr "" + diff --git a/sale_blanket_order/models/blanket_orders.py b/sale_blanket_order/models/blanket_orders.py index c84ff679ba5..9d0e5258fd4 100644 --- a/sale_blanket_order/models/blanket_orders.py +++ b/sale_blanket_order/models/blanket_orders.py @@ -1,7 +1,8 @@ -# coding: utf-8 # Copyright 2018 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import fields, models, api, _ +from datetime import datetime + +from odoo import fields, models, api, SUPERUSER_ID, _ from odoo.exceptions import UserError from odoo.tools import float_is_zero @@ -10,7 +11,7 @@ class BlanketOrder(models.Model): _name = 'sale.blanket.order' - _inherit = ['mail.thread', 'ir.needaction_mixin'] + _inherit = ['mail.thread', 'mail.activity.mixin'] _description = 'Blanket Order' @api.model @@ -25,6 +26,19 @@ def _default_currency(self): def _default_company(self): return self.env.user.company_id + @api.depends('line_ids.price_total') + def _amount_all(self): + for order in self: + amount_untaxed = amount_tax = 0.0 + for line in order.line_ids: + amount_untaxed += line.price_subtotal + amount_tax += line.price_tax + order.update({ + 'amount_untaxed': order.currency_id.round(amount_untaxed), + 'amount_tax': order.currency_id.round(amount_tax), + 'amount_total': amount_untaxed + amount_tax, + }) + name = fields.Char( default='Draft', readonly=True @@ -32,9 +46,19 @@ def _default_company(self): partner_id = fields.Many2one( 'res.partner', string='Partner', readonly=True, states={'draft': [('readonly', False)]}) - lines_ids = fields.One2many( + line_ids = fields.One2many( 'sale.blanket.order.line', 'order_id', string='Order lines', copy=True) + line_count = fields.Integer( + string='Sale Blanket Order Line count', + compute='_compute_line_count', + readonly=True + ) + product_id = fields.Many2one( + 'product.product', + related='line_ids.product_id', + string='Product', + ) pricelist_id = fields.Many2one( 'product.pricelist', string='Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)]}) @@ -46,9 +70,10 @@ def _default_company(self): confirmed = fields.Boolean() state = fields.Selection(selection=[ ('draft', 'Draft'), - ('opened', 'Open'), + ('open', 'Open'), + ('done', 'Done'), ('expired', 'Expired'), - ], compute='_compute_state', store=True) + ], compute='_compute_state', store=True, copy=False) validity_date = fields.Date( readonly=True, states={'draft': [('readonly', False)]}) @@ -71,21 +96,52 @@ def _default_company(self): states={'draft': [('readonly', False)]}) sale_count = fields.Integer(compute='_compute_sale_count') + fiscal_position_id = fields.Many2one('account.fiscal.position', + string='Fiscal Position') + + amount_untaxed = fields.Monetary(string='Untaxed Amount', store=True, + readonly=True, compute='_amount_all', + track_visibility='always') + amount_tax = fields.Monetary(string='Taxes', store=True, readonly=True, + compute='_amount_all') + amount_total = fields.Monetary(string='Total', store=True, readonly=True, + compute='_amount_all') + + # Fields use to filter in tree view + original_uom_qty = fields.Float( + string='Original quantity', compute='_compute_uom_qty', + search='_search_original_uom_qty', default=0.0) + ordered_uom_qty = fields.Float( + string='Ordered quantity', compute='_compute_uom_qty', + search='_search_ordered_uom_qty', default=0.0) + invoiced_uom_qty = fields.Float( + string='Invoiced quantity', compute='_compute_uom_qty', + search='_search_invoiced_uom_qty', default=0.0) + remaining_uom_qty = fields.Float( + string='Remaining quantity', compute='_compute_uom_qty', + search='_search_remaining_uom_qty', default=0.0) + delivered_uom_qty = fields.Float( + string='Delivered quantity', compute='_compute_uom_qty', + search='_search_delivered_uom_qty', default=0.0) + @api.multi def _get_sale_orders(self): - return self.mapped('lines_ids.sale_order_lines_ids.order_id') + return self.mapped('line_ids.sale_lines.order_id') + + @api.depends('line_ids') + def _compute_line_count(self): + self.line_count = len(self.mapped('line_ids')) @api.multi - @api.depends('lines_ids.remaining_qty') def _compute_sale_count(self): for blanket_order in self: blanket_order.sale_count = len(blanket_order._get_sale_orders()) @api.multi @api.depends( - 'lines_ids.remaining_qty', + 'line_ids.remaining_uom_qty', 'validity_date', - 'confirmed' + 'confirmed', ) def _compute_state(self): today = fields.Date.today() @@ -96,11 +152,19 @@ def _compute_state(self): order.state = 'draft' elif order.validity_date <= today: order.state = 'expired' - elif float_is_zero(sum(order.lines_ids.mapped('remaining_qty')), + elif float_is_zero(sum(order.line_ids.mapped('remaining_uom_qty')), precision_digits=precision): - order.state = 'expired' + order.state = 'done' else: - order.state = 'opened' + order.state = 'open' + + def _compute_uom_qty(self): + for bo in self: + bo.original_uom_qty = sum(bo.mapped('order_id.original_uom_qty')) + bo.ordered_uom_qty = sum(bo.mapped('order_id.ordered_uom_qty')) + bo.invoiced_uom_qty = sum(bo.mapped('order_id.invoiced_uom_qty')) + bo.delivered_uom_qty = sum(bo.mapped('order_id.delivered_uom_qty')) + bo.remaining_uom_qty = sum(bo.mapped('order_id.remaining_uom_qty')) @api.multi @api.onchange('partner_id') @@ -109,9 +173,11 @@ def onchange_partner_id(self): Update the following fields when the partner is changed: - Pricelist - Payment term + - Fiscal position """ if not self.partner_id: self.payment_term_id = False + self.fiscal_position_id = False return values = { @@ -121,6 +187,10 @@ def onchange_partner_id(self): 'payment_term_id': (self.partner_id.property_payment_term_id and self.partner_id.property_payment_term_id.id or False), + 'fiscal_position_id': self.env[ + 'account.fiscal.position'].with_context( + company_id=self.company_id.id).get_fiscal_position( + self.partner_id.id), } if self.partner_id.user_id: @@ -129,6 +199,15 @@ def onchange_partner_id(self): values['team_id'] = self.partner_id.team_id.id self.update(values) + @api.multi + def unlink(self): + for order in self: + if order.state not in ('draft', 'cancel'): + raise UserError(_( + 'You can not delete an open blanket order! ' + 'Try to cancel it before.')) + return super(BlanketOrder, self).unlink() + @api.multi def copy_data(self, default=None): if default is None: @@ -145,10 +224,16 @@ def _validate(self): assert order.validity_date > today, \ _("Validity date must be in the future") assert order.partner_id, _("Partner is mandatory") - assert len(order.lines_ids) > 0, _("Must have some lines") - order.lines_ids._validate() + assert len(order.line_ids) > 0, _("Must have some lines") + order.line_ids._validate() except AssertionError as e: - raise UserError(e.message) + raise UserError(e) + + @api.multi + def set_to_draft(self): + for order in self: + order.write({'state': 'draft'}) + return True @api.multi def action_confirm(self): @@ -162,61 +247,214 @@ def action_confirm(self): order.write({'confirmed': True, 'name': name}) return True + @api.multi + def action_cancel(self): + for order in self: + if order.sale_count > 0: + for so in order._get_sale_orders(): + if so.state not in ('cancel'): + raise UserError(_( + 'You can not delete a blanket order with opened ' + 'purchase orders! ' + 'Try to cancel them before.')) + order.write({'state': 'expired'}) + return True + @api.multi def action_view_sale_orders(self): sale_orders = self._get_sale_orders() action = self.env.ref('sale.action_orders').read()[0] if len(sale_orders) > 0: action['domain'] = [('id', 'in', sale_orders.ids)] + action['context'] = [('id', 'in', sale_orders.ids)] else: action = {'type': 'ir.actions.act_window_close'} return action + @api.multi + def action_view_sale_blanket_order_line(self): + action = self.env.ref( + 'sale_blanket_order' + '.act_open_sale_blanket_order_lines_view_tree').read()[0] + lines = self.mapped('line_ids') + if len(lines) > 0: + action['domain'] = [('id', 'in', lines.ids)] + return action + @api.model def expire_orders(self): today = fields.Date.today() expired_orders = self.search([ - ('state', '=', 'opened'), + ('state', '=', 'open'), ('validity_date', '<=', today), ]) expired_orders.modified(['validity_date']) expired_orders.recompute() + @api.model + def _search_original_uom_qty(self, operator, value): + bo_line_obj = self.env['sale.blanket.order.line'] + res = [] + bo_lines = bo_line_obj.search( + [('original_uom_qty', operator, value)]) + order_ids = bo_lines.mapped('order_id') + res.append(('id', 'in', order_ids.ids)) + return res + + @api.model + def _search_ordered_uom_qty(self, operator, value): + bo_line_obj = self.env['sale.blanket.order.line'] + res = [] + bo_lines = bo_line_obj.search( + [('ordered_uom_qty', operator, value)]) + order_ids = bo_lines.mapped('order_id') + res.append(('id', 'in', order_ids.ids)) + return res + + @api.model + def _search_invoiced_uom_qty(self, operator, value): + bo_line_obj = self.env['sale.blanket.order.line'] + res = [] + bo_lines = bo_line_obj.search( + [('invoiced_uom_qty', operator, value)]) + order_ids = bo_lines.mapped('order_id') + res.append(('id', 'in', order_ids.ids)) + return res + + @api.model + def _search_delivered_uom_qty(self, operator, value): + bo_line_obj = self.env['sale.blanket.order.line'] + res = [] + bo_lines = bo_line_obj.search( + [('delivered_uom_qty', operator, value)]) + order_ids = bo_lines.mapped('order_id') + res.append(('id', 'in', order_ids.ids)) + return res + + @api.model + def _search_remaining_uom_qty(self, operator, value): + bo_line_obj = self.env['sale.blanket.order.line'] + res = [] + bo_lines = bo_line_obj.search( + [('remaining_uom_qty', operator, value)]) + order_ids = bo_lines.mapped('order_id') + res.append(('id', 'in', order_ids.ids)) + return res + class BlanketOrderLine(models.Model): _name = 'sale.blanket.order.line' _description = 'Blanket Order Line' + _inherit = ['mail.thread', 'mail.activity.mixin'] + + @api.depends('original_uom_qty', 'price_unit', 'taxes_id', + 'order_id.partner_id', 'product_id', 'currency_id') + def _compute_amount(self): + for line in self: + price = line.price_unit + taxes = line.taxes_id.compute_all( + price, line.currency_id, + line.original_uom_qty, + product=line.product_id, + partner=line.order_id.partner_id) + line.update({ + 'price_tax': sum( + t.get('amount', 0.0) for t in taxes.get('taxes', [])), + 'price_total': taxes['total_included'], + 'price_subtotal': taxes['total_excluded'], + }) + name = fields.Char('Description', track_visibility='onchange') sequence = fields.Integer() order_id = fields.Many2one( 'sale.blanket.order', required=True, ondelete='cascade') product_id = fields.Many2one( - 'product.product', string='Product', required=True) + 'product.product', string='Product', required=True, + domain=[('sale_ok', '=', True)]) product_uom = fields.Many2one( 'product.uom', string='Unit of Measure', required=True) - price_unit = fields.Float(string='Price', required=True) - original_qty = fields.Float( + price_unit = fields.Float(string='Price', required=True, + digits=dp.get_precision('Product Price')) + taxes_id = fields.Many2many('account.tax', string='Taxes', + domain=['|', ('active', '=', False), + ('active', '=', True)]) + date_schedule = fields.Date(string='Scheduled Date') + original_uom_qty = fields.Float( string='Original quantity', required=True, default=1, digits=dp.get_precision('Product Unit of Measure')) - ordered_qty = fields.Float( - string='Ordered quantity', compute='_compute_quantities', store=True) - invoiced_qty = fields.Float( - string='Invoiced quantity', compute='_compute_quantities', store=True) + ordered_uom_qty = fields.Float( + string='Ordered quantity', compute='_compute_quantities', + store=True) + invoiced_uom_qty = fields.Float( + string='Invoiced quantity', compute='_compute_quantities', + store=True) + remaining_uom_qty = fields.Float( + string='Remaining quantity', compute='_compute_quantities', + store=True) remaining_qty = fields.Float( - string='Remaining quantity', compute='_compute_quantities', store=True) - delivered_qty = fields.Float( - string='Delivered quantity', compute='_compute_quantities', store=True) - sale_order_lines_ids = fields.One2many( - 'sale.order.line', 'blanket_line_id', string='Sale order lines') + string='Remaining quantity in base UoM', compute='_compute_quantities', + store=True) + delivered_uom_qty = fields.Float( + string='Delivered quantity', compute='_compute_quantities', + store=True) + sale_lines = fields.One2many( + 'sale.order.line', 'blanket_order_line', string='Sale order lines', + readonly=True, copy=False) company_id = fields.Many2one( 'res.company', related='order_id.company_id', store=True, readonly=True) + currency_id = fields.Many2one( + 'res.currency', related='order_id.currency_id', readonly=True) + partner_id = fields.Many2one( + related='order_id.partner_id', + string='Customer', + readonly=True) + user_id = fields.Many2one( + related='order_id.user_id', string='Responsible', + readonly=True) + payment_term_id = fields.Many2one( + related='order_id.payment_term_id', string='Payment Terms', + readonly=True) + pricelist_id = fields.Many2one( + related='order_id.pricelist_id', string='Pricelist', + readonly=True) + + price_subtotal = fields.Monetary(compute='_compute_amount', + string='Subtotal', store=True) + price_total = fields.Monetary(compute='_compute_amount', string='Total', + store=True) + price_tax = fields.Float(compute='_compute_amount', string='Tax', + store=True) + + def _format_date(self, date): + # format date following user language + lang_model = self.env['res.lang'] + lang = lang_model._lang_get(self.env.user.lang) + date_format = lang.date_format + return datetime.strftime( + fields.Date.from_string(date), date_format) + + def name_get(self): + result = [] + if self.env.context.get('from_sale_order'): + for record in self: + res = "[%s]" % record.order_id.name + if record.date_schedule: + formatted_date = self._format_date(record.date_schedule) + res += ' - %s: %s' % ( + _('Date Scheduled'), formatted_date) + res += ' (%s: %s %s)' % (_('remaining'), + record.remaining_uom_qty, + record.product_uom.name) + result.append((record.id, res)) + return result + return super(BlanketOrderLine, self).name_get() def _get_real_price_currency(self, product, rule_id, qty, uom, pricelist_id): """Retrieve the price before applying the pricelist :param obj product: object of current product record - :parem float qty: total quentity of product + :param float qty: total quentity of product :param tuple price_and_rule: tuple(price, suitable_rule) coming from pricelist computation :param obj uom: unit of measure of current order line @@ -281,13 +519,13 @@ def _get_display_price(self, product): if self.order_id.pricelist_id.discount_policy == 'with_discount': return product.with_context(pricelist=pricelist.id).price final_price, rule_id = pricelist.get_product_price_rule( - self.product_id, self.original_qty or 1.0, partner) + self.product_id, self.original_uom_qty or 1.0, partner) context_partner = dict(self.env.context, partner_id=partner.id, date=fields.Date.today()) base_price, currency_id = self.with_context(context_partner)\ ._get_real_price_currency( - self.product_id, rule_id, self.original_qty, self.product_uom, - pricelist.id) + self.product_id, rule_id, self.original_uom_qty, + self.product_uom, pricelist.id) if currency_id != pricelist.currency_id.id: currency = self.env['res.currency'].browse(currency_id) base_price = currency.with_context(context_partner).compute( @@ -296,28 +534,65 @@ def _get_display_price(self, product): return max(base_price, final_price) @api.multi - @api.onchange('product_id', 'original_qty') + @api.onchange('product_id', 'original_uom_qty') def onchange_product(self): + precision = self.env['decimal.precision'].precision_get( + 'Product Unit of Measure') if self.product_id: - self.product_uom = self.product_id.uom_id.id - if self.order_id.pricelist_id and self.order_id.partner_id: + name = self.product_id.name + if not self.product_uom: + self.product_uom = self.product_id.uom_id.id + if self.order_id.partner_id and \ + float_is_zero(self.price_unit, precision_digits=precision): self.price_unit = self._get_display_price(self.product_id) + if self.product_id.code: + name = '[%s] %s' % (name, self.product_id.code) + if self.product_id.description_sale: + name += '\n' + self.product_id.description_sale + self.name = name + + fpos = self.order_id.fiscal_position_id + if self.env.uid == SUPERUSER_ID: + company_id = self.env.user.company_id.id + self.taxes_id = fpos.map_tax( + self.product_id.supplier_taxes_id.filtered( + lambda r: r.company_id.id == company_id)) + else: + self.taxes_id = fpos.map_tax(self.product_id.supplier_taxes_id) @api.multi @api.depends( - 'sale_order_lines_ids.blanket_line_id', - 'sale_order_lines_ids.product_uom_qty', - 'sale_order_lines_ids.qty_delivered', - 'sale_order_lines_ids.qty_invoiced', - 'original_qty' + 'sale_lines.order_id.state', + 'sale_lines.blanket_order_line', + 'sale_lines.product_uom_qty', + 'sale_lines.product_uom', + 'sale_lines.qty_delivered', + 'sale_lines.qty_invoiced', + 'original_uom_qty', + 'product_uom', ) def _compute_quantities(self): for line in self: - sale_lines = line.sale_order_lines_ids - line.ordered_qty = sum(l.product_uom_qty for l in sale_lines) - line.invoiced_qty = sum(l.qty_invoiced for l in sale_lines) - line.delivered_qty = sum(l.qty_delivered for l in sale_lines) - line.remaining_qty = line.original_qty - line.ordered_qty + sale_lines = line.sale_lines + line.ordered_uom_qty = sum( + l.product_uom._compute_quantity( + l.product_uom_qty, line.product_uom) + for l in sale_lines if l.order_id.state != 'cancel' and + l.product_id == line.product_id) + line.invoiced_uom_qty = sum( + l.product_uom._compute_quantity( + l.qty_invoiced, line.product_uom) + for l in sale_lines if l.order_id.state != 'cancel' and + l.product_id == line.product_id) + line.delivered_uom_qty = sum( + l.product_uom._compute_quantity( + l.qty_delivered, line.product_uom) + for l in sale_lines if l.order_id.state != 'cancel' and + l.product_id == line.product_id) + line.remaining_uom_qty = line.original_uom_qty - \ + line.ordered_uom_qty + line.remaining_qty = line.product_uom._compute_quantity( + line.remaining_uom_qty, line.product_id.uom_id) @api.multi def _validate(self): @@ -325,7 +600,7 @@ def _validate(self): for line in self: assert line.price_unit > 0.0, \ _("Price must be greater than zero") - assert line.original_qty > 0.0, \ + assert line.original_uom_qty > 0.0, \ _("Quantity must be greater than zero") except AssertionError as e: - raise UserError(e.message) + raise UserError(e) diff --git a/sale_blanket_order/models/sale_config_settings.py b/sale_blanket_order/models/sale_config_settings.py index 5853a195441..b2d237d1e4d 100644 --- a/sale_blanket_order/models/sale_config_settings.py +++ b/sale_blanket_order/models/sale_config_settings.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright 2018 ACSONE SA/NV # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). @@ -7,7 +6,7 @@ class SaleConfigSettings(models.TransientModel): - _inherit = 'sale.config.settings' + _inherit = 'res.config.settings' group_blanket_disable_adding_lines = fields.Boolean( string='Disable adding more lines to SOs', diff --git a/sale_blanket_order/models/sale_orders.py b/sale_blanket_order/models/sale_orders.py index 0fb0877cba7..48e076d17b1 100644 --- a/sale_blanket_order/models/sale_orders.py +++ b/sale_blanket_order/models/sale_orders.py @@ -1,7 +1,9 @@ -# coding: utf-8 # Copyright 2018 ACSONE SA/NV +# Copyright 2019 Eficent and IT Consulting Services, S.L. # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -from odoo import fields, models +from odoo import api, fields, models, _ +from datetime import date, timedelta +from odoo.exceptions import ValidationError class SaleOrder(models.Model): @@ -9,11 +11,140 @@ class SaleOrder(models.Model): blanket_order_id = fields.Many2one( 'sale.blanket.order', string='Origin blanket order', - related='order_line.blanket_line_id.order_id', + related='order_line.blanket_order_line.order_id', readonly=True) + @api.model + def _check_exchausted_blanket_order_line(self): + return any(line.blanket_order_line.remaining_qty < 0.0 for + line in self.order_line) + + @api.multi + def button_confirm(self): + res = super(SaleOrder, self).button_confirm() + for order in self: + if order._check_exchausted_blanket_order_line(): + raise ValidationError( + _('Cannot confirm order %s as one of the lines refers ' + 'to a blanket order that has no remaining quantity.') + % order.name) + return res + + @api.constrains('partner_id') + def check_partner_id(self): + for line in self.order_line: + if line.blanket_order_line: + if line.blanket_order_line.partner_id != \ + self.partner_id: + raise ValidationError(_( + 'The customer must be equal to the ' + 'blanket order lines customer')) + class SaleOrderLine(models.Model): _inherit = 'sale.order.line' - blanket_line_id = fields.Many2one('sale.blanket.order.line') + blanket_order_line = fields.Many2one( + 'sale.blanket.order.line', + string='Blanket Order line', + copy=False) + + def _get_assigned_bo_line(self, bo_lines): + # We get the blanket order line with enough quantity and closest + # scheduled date + assigned_bo_line = False + date_planned = date.today() + date_delta = timedelta(days=365) + for line in bo_lines.filtered(lambda l: l.date_schedule): + date_schedule = fields.Date.from_string(line.date_schedule) + if date_schedule and \ + abs(date_schedule - date_planned) < date_delta: + assigned_bo_line = line + date_delta = abs(date_schedule - date_planned) + if assigned_bo_line: + return assigned_bo_line + non_date_bo_lines = bo_lines.filtered(lambda l: not l.date_schedule) + if non_date_bo_lines: + return non_date_bo_lines[0] + + def _get_eligible_bo_lines_domain(self, base_qty): + filters = [ + ('product_id', '=', self.product_id.id), + ('remaining_qty', '>=', base_qty), + ('currency_id', '=', self.order_id.currency_id.id), + ('order_id.state', '=', 'open')] + if self.order_id.partner_id: + filters.append( + ('partner_id', '=', self.order_id.partner_id.id)) + return filters + + def _get_eligible_bo_lines(self): + base_qty = self.product_uom._compute_quantity( + self.product_uom_qty, self.product_id.uom_id) + filters = self._get_eligible_bo_lines_domain(base_qty) + return self.env['sale.blanket.order.line'].search(filters) + + @api.multi + def get_assigned_bo_line(self): + self.ensure_one() + eligible_bo_lines = self._get_eligible_bo_lines() + if eligible_bo_lines: + if not self.blanket_order_line or self.blanket_order_line \ + not in eligible_bo_lines: + self.blanket_order_line = \ + self._get_assigned_bo_line(eligible_bo_lines) + else: + self.blanket_order_line = False + self.onchange_blanket_order_line() + return {'domain': {'blanket_order_line': [ + ('id', 'in', eligible_bo_lines.ids)]}} + + @api.onchange('product_id', 'order_partner_id') + def onchange_product_id(self): + # If product has changed remove the relation with blanket order line + if self.product_id: + return self.get_assigned_bo_line() + return + + @api.onchange('product_uom_qty', 'product_uom') + def product_uom_change(self): + res = super(SaleOrderLine, self).product_uom_change() + if self.product_id and not self.env.context.get( + 'skip_blanket_find', False): + return self.get_assigned_bo_line() + return res + + @api.onchange('blanket_order_line') + def onchange_blanket_order_line(self): + bol = self.blanket_order_line + if bol: + self.product_id = bol.product_id + if bol.product_uom != self.product_uom: + price_unit = bol.product_uom._compute_price( + bol.price_unit, self.product_uom) + else: + price_unit = bol.price_unit + self.price_unit = price_unit + if bol.taxes_id: + self.tax_id = bol.taxes_id + else: + self._compute_tax_id() + self.with_context(skip_blanket_find=True).product_uom_change() + + @api.constrains('product_id') + def check_product_id(self): + if self.blanket_order_line and \ + self.product_id != self.blanket_order_line.product_id: + raise ValidationError(_( + 'The product in the blanket order and in the ' + 'sales order must match')) + + @api.constrains('currency_id') + def check_currency(self): + for line in self: + if line.blanket_order_line: + if line.currency_id != \ + line.blanket_order_line.order_id.currency_id: + raise ValidationError(_( + 'The currency of the blanket order must match with ' + 'that of the sale order.')) diff --git a/sale_blanket_order/readme/CONTRIBUTORS.rst b/sale_blanket_order/readme/CONTRIBUTORS.rst new file mode 100644 index 00000000000..a58dd1bee20 --- /dev/null +++ b/sale_blanket_order/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* André Pereira (https://www.acsone.eu/) +* Adrià Gil Sorribes (https://www.eficent.com/) +* Jordi Ballester Alomar diff --git a/sale_blanket_order/readme/DESCRIPTION.rst b/sale_blanket_order/readme/DESCRIPTION.rst new file mode 100644 index 00000000000..bd7b2d7a03a --- /dev/null +++ b/sale_blanket_order/readme/DESCRIPTION.rst @@ -0,0 +1,4 @@ +A blanket order is a pre-agreement to sell a certain number of quantities of +products at a specific price. From a confirmed blanket order, the users can +create new sale orders at such price, until the blanket order expires, either +due to reaching the validity date or exhausting all the quantities of products. diff --git a/sale_blanket_order/readme/USAGE.rst b/sale_blanket_order/readme/USAGE.rst new file mode 100644 index 00000000000..2b2252bdd36 --- /dev/null +++ b/sale_blanket_order/readme/USAGE.rst @@ -0,0 +1,53 @@ +A new menu in the Sales area is created, allowing users to create new blanket orders. + +To create a new Sale Blanket Order go to the sale menu in the Sales section: + +.. image:: /sale_blanket_order/static/description/BO_menu.png + :alt: Blanket Orders menu + +Hitting the button create will open the form view in which we can introduce the following +information: + +* Vendor +* Salesperson +* Payment Terms +* Validity date +* Order lines: + * Product + * Accorded price + * Original, Ordered, Invoiced, Received and Remaining quantities +* Terms and Conditions of the Blanket Order + +.. image:: /sale_blanket_order/static/description/BO_form.png + :alt: Blanket Orders form + +From the form, once the Blanket Order has been confirmed and its state is open, the user can +create a Sale Order, check the Sale Orders associated to the Blanket Order and/or +see the Blanket Order lines associated to the BO. + +.. image:: /sale_blanket_order/static/description/BO_actions.png + :alt: Actions that can be done from Blanket Order + +Hitting the button Create Sale Order will open a wizard that will ask for the amount of each +product in the BO lines for which the Sale Order will be created. + +.. image:: /sale_blanket_order/static/description/PO_from_BO.png + :alt: Create Sale Order from Blanket Order + +Installing this module will add an additional menu which will show all the blanket order lines +currently defined in the system. From this list the user can create customized Sale Orders +selecting the lines for which the PO (or POs if the customers are different) is (are) created. + +.. image:: /sale_blanket_order/static/description/BO_lines.png + :alt: Blanket Order lines and actions + +In the Sale Order form one field is added in the PO lines, the Blanket Order line field. This +field keeps track to which Blanket Order line the PO line is associated. Upon adding a new product +in a newly created Sale Order a blanket order line will be suggested depending on the following +factors: + +* Closer Validity date +* Remaining quantity > Quantity introduced in the Sale Order line + +.. image:: /sale_blanket_order/static/description/PO_BOLine.png + :alt: New field added in Sale Order Line diff --git a/sale_blanket_order/report/templates.xml b/sale_blanket_order/report/templates.xml index 48731b3f4e0..0a77da0060b 100644 --- a/sale_blanket_order/report/templates.xml +++ b/sale_blanket_order/report/templates.xml @@ -1,7 +1,7 @@