From e57ac16476bf127c9bd989092e4247e7c12bd5be Mon Sep 17 00:00:00 2001 From: Nicolas Martinelli <nim@odoo.com> Date: Thu, 27 Aug 2015 12:34:25 +0200 Subject: [PATCH] [IMP] sale_stock: adaptation due to the new Sale module Major changes: - No invoicing anymore - Migration to new api Reason: complete rewrite of the Sale module. Responsible: fp, dbo, nim --- addons/sale_stock/__openerp__.py | 11 - addons/sale_stock/account_invoice_view.xml | 4 +- addons/sale_stock/company.py | 21 +- addons/sale_stock/company_view.xml | 2 +- addons/sale_stock/report/sale_report.py | 29 +- addons/sale_stock/report/sale_report_view.xml | 21 - addons/sale_stock/res_config.py | 82 +-- addons/sale_stock/res_config_view.xml | 3 +- addons/sale_stock/sale_stock.py | 672 +++++------------- addons/sale_stock/sale_stock_demo.xml | 13 +- addons/sale_stock/sale_stock_demo.yml | 1 - addons/sale_stock/sale_stock_view.xml | 137 +--- addons/sale_stock/sale_stock_workflow.xml | 6 - addons/sale_stock/stock_view.xml | 6 +- .../test/cancel_order_sale_stock.yml | 94 --- .../sale_stock/test/picking_order_policy.yml | 217 ------ .../sale_stock/test/prepaid_order_policy.yml | 30 - .../test/sale_order_canceled_line.yml | 45 -- .../sale_stock/test/sale_order_onchange.yml | 33 - addons/sale_stock/test/sale_stock_users.yml | 57 -- addons/sale_stock/tests/__init__.py | 3 + addons/sale_stock/tests/test_sale_stock.py | 174 +++++ 22 files changed, 441 insertions(+), 1220 deletions(-) delete mode 100644 addons/sale_stock/report/sale_report_view.xml delete mode 100644 addons/sale_stock/sale_stock_workflow.xml delete mode 100644 addons/sale_stock/test/cancel_order_sale_stock.yml delete mode 100644 addons/sale_stock/test/picking_order_policy.yml delete mode 100644 addons/sale_stock/test/prepaid_order_policy.yml delete mode 100644 addons/sale_stock/test/sale_order_canceled_line.yml delete mode 100644 addons/sale_stock/test/sale_order_onchange.yml delete mode 100644 addons/sale_stock/test/sale_stock_users.yml create mode 100644 addons/sale_stock/tests/__init__.py create mode 100644 addons/sale_stock/tests/test_sale_stock.py diff --git a/addons/sale_stock/__openerp__.py b/addons/sale_stock/__openerp__.py index 7944e8a8e064..a2672e107f9b 100644 --- a/addons/sale_stock/__openerp__.py +++ b/addons/sale_stock/__openerp__.py @@ -31,22 +31,11 @@ You can choose flexible invoicing methods: 'security/ir.model.access.csv', 'company_view.xml', 'sale_stock_view.xml', - 'sale_stock_workflow.xml', 'stock_view.xml', 'res_config_view.xml', - 'report/sale_report_view.xml', 'account_invoice_view.xml', ], 'demo': ['sale_stock_demo.xml'], - 'test': [ - '../account/test/account_minimal_test.xml', - 'test/sale_stock_users.yml', - 'test/cancel_order_sale_stock.yml', - 'test/picking_order_policy.yml', - 'test/prepaid_order_policy.yml', - 'test/sale_order_onchange.yml', - 'test/sale_order_canceled_line.yml', - ], 'installable': True, 'auto_install': True, } diff --git a/addons/sale_stock/account_invoice_view.xml b/addons/sale_stock/account_invoice_view.xml index e9b8c44cba32..be326a46270c 100644 --- a/addons/sale_stock/account_invoice_view.xml +++ b/addons/sale_stock/account_invoice_view.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <odoo> - <record id="view_invoice_form_inherit" model="ir.ui.view"> + <record id="invoice_form_inherit_sale_stock" model="ir.ui.view"> <field name="name">account.invoice.form.sale.stock</field> <field name="model">account.invoice</field> <field name="inherit_id" ref="account.invoice_form"/> @@ -13,7 +13,7 @@ </field> </record> - <template id="report_invoice_incoterm" inherit_id="account.report_invoice_document"> + <template id="report_invoice_document_inherit_sale_stock" inherit_id="account.report_invoice_document"> <xpath expr="//div[@name='reference']" position="after"> <div class="col-xs-2" t-if="o.incoterms_id" groups="sale.group_display_incoterm"> <strong>Incoterms:</strong> diff --git a/addons/sale_stock/company.py b/addons/sale_stock/company.py index a991f14e1cc0..7b1e382f1aac 100644 --- a/addons/sale_stock/company.py +++ b/addons/sale_stock/company.py @@ -1,18 +1,13 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from openerp.osv import fields, osv +from openerp import fields, models -class company(osv.osv): +class company(models.Model): _inherit = 'res.company' - _columns = { - 'security_lead': fields.float( - 'Sales Safety Days', required=True, - help="Margin of error for dates promised to customers. "\ - "Products will be scheduled for procurement and delivery "\ - "that many days earlier than the actual promised date, to "\ - "cope with unexpected delays in the supply chain."), - } - _defaults = { - 'security_lead': 0.0, - } + + security_lead = fields.Float('Sales Safety Days', required=True, default = 0.0, + help="Margin of error for dates promised to customers. "\ + "Products will be scheduled for procurement and delivery "\ + "that many days earlier than the actual promised date, to "\ + "cope with unexpected delays in the supply chain.") diff --git a/addons/sale_stock/company_view.xml b/addons/sale_stock/company_view.xml index 032c264ebd6b..4866d30d6d6c 100644 --- a/addons/sale_stock/company_view.xml +++ b/addons/sale_stock/company_view.xml @@ -1,7 +1,7 @@ <?xml version="1.0" ?> <openerp> <data> - <record id="mrp_company" model="ir.ui.view"> + <record id="view_company_form_inherit_sale_stock" model="ir.ui.view"> <field name="name">res.company.mrp.config</field> <field name="model">res.company</field> <field name="priority">24</field> diff --git a/addons/sale_stock/report/sale_report.py b/addons/sale_stock/report/sale_report.py index 47d8551019fd..3475684df444 100644 --- a/addons/sale_stock/report/sale_report.py +++ b/addons/sale_stock/report/sale_report.py @@ -1,30 +1,15 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. - -from openerp.osv import fields, osv -from openerp import tools -class sale_report(osv.osv): +from openerp import fields, models + +class SaleReport(models.Model): _inherit = "sale.report" - _columns = { - 'shipped': fields.boolean('Shipped', readonly=True), - 'shipped_qty_1': fields.integer('# of Shipped Lines', readonly=True), - 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse',readonly=True), - 'state': fields.selection([ - ('draft', 'Draft Quotation'), - ('sent', 'Quotation Sent'), - ('waiting_date', 'Waiting Schedule'), - ('manual', 'Sale to Invoice'), - ('progress', 'Sale Order'), - ('shipping_except', 'Shipping Exception'), - ('invoice_except', 'Invoice Exception'), - ('done', 'Done'), - ('cancel', 'Cancelled') - ], 'Order Status', readonly=True), - } + + warehouse_id = fields.Many2one('stock.warehouse', 'Warehouse', readonly=True) def _select(self): - return super(sale_report, self)._select() + ", s.warehouse_id as warehouse_id, s.shipped, s.shipped::integer as shipped_qty_1" + return super(SaleReport, self)._select() + ", s.warehouse_id as warehouse_id" def _group_by(self): - return super(sale_report, self)._group_by() + ", s.warehouse_id, s.shipped" + return super(SaleReport, self)._group_by() + ", s.warehouse_id" diff --git a/addons/sale_stock/report/sale_report_view.xml b/addons/sale_stock/report/sale_report_view.xml deleted file mode 100644 index 0ef9e9df3a69..000000000000 --- a/addons/sale_stock/report/sale_report_view.xml +++ /dev/null @@ -1,21 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<openerp> - <data> - <!-- Filters too specific to propose directly to the user - <record id="view_order_product_search_sale_stock_inherit" model="ir.ui.view"> - <field name="name">sale.report.search.sale.stock</field> - <field name="model">sale.report</field> - <field name="inherit_id" ref="sale.view_order_product_search"/> - <field name="arch" type="xml"> - <filter name="Sales" position="after"> - <separator/> - <filter string="Picked" domain="[('shipped','=',True)]"/> - </filter> - <xpath expr="//group/filter[@name='status']" position="after"> - <filter string="Warehouse" context="{'group_by':'warehouse_id'}"/> - </xpath> - </field> - </record> - --> - </data> -</openerp> diff --git a/addons/sale_stock/res_config.py b/addons/sale_stock/res_config.py index be0689a4308e..6852a73d3de5 100644 --- a/addons/sale_stock/res_config.py +++ b/addons/sale_stock/res_config.py @@ -1,59 +1,47 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -import openerp from openerp import SUPERUSER_ID -from openerp.osv import fields, osv -from openerp.tools.translate import _ +from openerp import api, fields, models, _ +from openerp.exceptions import AccessError -class sale_configuration(osv.osv_memory): +class SaleConfiguration(models.TransientModel): _inherit = 'sale.config.settings' - _columns = { - 'default_order_policy': fields.selection([ - ('manual', 'Invoice based on sales order'), - ('picking', 'Invoice based on delivery orders')], - 'Invoicing Method', default_model='sale.order'), - 'module_delivery': fields.selection([ - (0, 'No shipping costs on sales orders'), - (1, 'Allow adding shipping costs') - ], "Shipping"), - 'default_picking_policy' : fields.selection([ - (0, 'Ship products when some are available, and allow back orders'), - (1, 'Ship all products at once, without back orders') - ], "Default Shipping Policy"), - 'group_mrp_properties': fields.selection([ - (0, "Don't use manufacturing properties (recommended as its easier)"), - (1, 'Allow setting manufacturing order properties per order line (avanced)') - ], "Properties on SO Lines", - implied_group='sale.group_mrp_properties', - help="Allows you to tag sales order lines with properties."), - 'group_route_so_lines': fields.selection([ - (0, 'No order specific routes like MTO or drop shipping'), - (1, 'Choose specific routes on sales order lines (advanced)') - ], "Order Routing", - implied_group='sale_stock.group_route_so_lines'), - } - - _defaults = { - 'default_order_policy': 'manual', - } - - def get_default_sale_config(self, cr, uid, ids, context=None): - ir_values = self.pool.get('ir.values') - default_picking_policy = ir_values.get_default(cr, uid, 'sale.order', 'picking_policy') + module_delivery = fields.Selection([ + (0, 'No shipping costs on sales orders'), + (1, 'Allow adding shipping costs') + ], "Shipping") + default_picking_policy = fields.Selection([ + (0, 'Ship products when some are available, and allow back orders'), + (1, 'Ship all products at once, without back orders') + ], "Default Shipping Policy") + group_mrp_properties = fields.Selection([ + (0, "Don't use manufacturing properties (recommended as its easier)"), + (1, 'Allow setting manufacturing order properties per order line (avanced)') + ], "Properties on SO Lines", + implied_group='sale.group_mrp_properties', + help="Allows you to tag sales order lines with properties.") + group_route_so_lines = fields.Selection([ + (0, 'No order specific routes like MTO or drop shipping'), + (1, 'Choose specific routes on sales order lines (advanced)') + ], "Order Routing", + implied_group='sale_stock.group_route_so_lines') + + @api.multi + def get_default_sale_config(self): + default_picking_policy = self.env['ir.values'].get_default('sale.order', 'picking_policy') return { - 'default_picking_policy': (default_picking_policy == 'one') and 1 or 0, + 'default_picking_policy': 1 if default_picking_policy == 'one' else 0, } - def set_sale_defaults(self, cr, uid, ids, context=None): - if not self.pool['res.users']._is_admin(cr, uid, [uid]): - raise openerp.exceptions.AccessError(_("Only administrators can change the settings")) - ir_values = self.pool.get('ir.values') - wizard = self.browse(cr, uid, ids)[0] + @api.multi + def set_sale_defaults(self): + self.ensure_one() + if not self.env.user._is_admin(): + raise AccessError(_("Only administrators can change the settings")) - default_picking_policy = 'one' if wizard.default_picking_policy else 'direct' - ir_values.set_default(cr, SUPERUSER_ID, 'sale.order', 'picking_policy', default_picking_policy) - res = super(sale_configuration, self).set_sale_defaults(cr, uid, ids, context) + default_picking_policy = 'one' if self.default_picking_policy else 'direct' + self.env['ir.values'].sudo().set_default('sale.order', 'picking_policy', default_picking_policy) + res = super(SaleConfiguration, self).set_sale_defaults() return res - diff --git a/addons/sale_stock/res_config_view.xml b/addons/sale_stock/res_config_view.xml index f00e142b2aa8..1babd1df6445 100644 --- a/addons/sale_stock/res_config_view.xml +++ b/addons/sale_stock/res_config_view.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <odoo> - <record id="view_sales_config_sale_stock" model="ir.ui.view"> + <record id="view_sales_config_inherit_sale_stock" model="ir.ui.view"> <field name="name">sale settings</field> <field name="model">sale.config.settings</field> <field name="inherit_id" ref="sale.view_sales_config"/> @@ -12,7 +12,6 @@ </group> </xpath> <xpath expr="//group[@id='sale']" position="inside"> - <field name="default_order_policy" widget="radio"/> <field name="group_route_so_lines" widget="radio"/> <field name="group_mrp_properties" widget="radio" groups="base.group_no_one"/> </xpath> diff --git a/addons/sale_stock/sale_stock.py b/addons/sale_stock/sale_stock.py index f49adcb3c172..9b9c4900e0da 100644 --- a/addons/sale_stock/sale_stock.py +++ b/addons/sale_stock/sale_stock.py @@ -1,516 +1,230 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. + from datetime import datetime, timedelta -from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP, float_compare -from openerp.osv import fields, osv -from openerp.tools.safe_eval import safe_eval as eval -from openerp.tools.translate import _ -import pytz -from openerp import SUPERUSER_ID -from openerp.exceptions import UserError - -class sale_order(osv.osv): - _inherit = "sale.order" +from openerp import api, fields, models, _ +from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT, float_compare - def _get_default_warehouse(self, cr, uid, context=None): - company_id = self.pool.get('res.users')._get_company(cr, uid, context=context) - warehouse_ids = self.pool.get('stock.warehouse').search(cr, uid, [('company_id', '=', company_id)], context=context) - if not warehouse_ids: - return False - return warehouse_ids[0] - - def _get_shipped(self, cr, uid, ids, name, args, context=None): - res = {} - for sale in self.browse(cr, uid, ids, context=context): - group = sale.procurement_group_id - if group: - res[sale.id] = all([proc.state in ['cancel', 'done'] for proc in group.procurement_ids]) - else: - res[sale.id] = False - return res - def _get_orders(self, cr, uid, ids, context=None): - res = set() - for move in self.browse(cr, uid, ids, context=context): - if move.procurement_id and move.procurement_id.sale_line_id: - res.add(move.procurement_id.sale_line_id.order_id.id) - return list(res) - - def _get_orders_procurements(self, cr, uid, ids, context=None): - res = set() - for proc in self.pool.get('procurement.order').browse(cr, uid, ids, context=context): - if proc.state =='done' and proc.sale_line_id: - res.add(proc.sale_line_id.order_id.id) - return list(res) - - def _get_picking_ids(self, cr, uid, ids, name, args, context=None): - res = {} - StockPicking = self.pool.get('stock.picking') - for sale in self.browse(cr, uid, ids, context=context): - picking_ids = [] - if sale.procurement_group_id: - picking_ids = StockPicking.search(cr, uid, [('group_id', '=', sale.procurement_group_id.id)], context=context) - res[sale.id] = { - 'picking_ids': picking_ids, - 'delivery_count': len(picking_ids) - } - return res +class SaleOrder(models.Model): + _inherit = "sale.order" - def _prepare_order_line_procurement(self, cr, uid, order, line, group_id=False, context=None): - vals = super(sale_order, self)._prepare_order_line_procurement(cr, uid, order, line, group_id=group_id, context=context) - location_id = order.partner_shipping_id.property_stock_customer.id - vals['location_id'] = location_id - routes = line.route_id and [(4, line.route_id.id)] or [] - vals['route_ids'] = routes - vals['warehouse_id'] = order.warehouse_id and order.warehouse_id.id or False - vals['partner_dest_id'] = order.partner_shipping_id.id - vals['invoice_state'] = (order.order_policy == 'picking') and '2binvoiced' or 'none' - return vals + @api.model + def _default_warehouse_id(self): + company = self.env.user.company_id.id + warehouse_ids = self.env['stock.warehouse'].search([('company_id', '=', company)], limit=1) + return warehouse_ids + + incoterm = fields.Many2one('stock.incoterms', 'Incoterms', help="International Commercial Terms are a series of predefined commercial terms used in international transactions.") + picking_policy = fields.Selection([ + ('direct', 'Deliver each product when available'), + ('one', 'Deliver all products at once')], + string='Shipping Policy', required=True, readonly=True, default='direct', + states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}) + warehouse_id = fields.Many2one('stock.warehouse', string='Warehouse', + required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, + default=_default_warehouse_id) + picking_ids = fields.One2many('stock.picking', compute='_compute_picking_ids', string='Picking associated to this sale') + delivery_count = fields.Integer(string='Delivery Orders', compute='_compute_picking_ids') + + @api.multi + @api.depends('procurement_group_id') + def _compute_picking_ids(self): + for order in self: + if not order.procurement_group_id: + order.picking_ids = [] + order.delivery_count = 0 + else: + order.picking_ids = self.env['stock.picking'].search([('group_id', '=', order.procurement_group_id.id)]).ids + order.delivery_count = len(order.picking_ids.ids) - def _prepare_invoice(self, cr, uid, order, lines, context=None): - if context is None: - context = {} - invoice_vals = super(sale_order, self)._prepare_invoice(cr, uid, order, lines, context=context) - invoice_vals['incoterms_id'] = order.incoterm.id or False - return invoice_vals + @api.onchange('warehouse_id') + def _onchange_warehouse_id(self): + if self.warehouse_id.company_id: + self.company_id = self.warehouse_id.company_id.id - _columns = { - 'incoterm': fields.many2one('stock.incoterms', 'Incoterms', help="International Commercial Terms are a series of predefined commercial terms used in international transactions."), - 'picking_policy': fields.selection([('direct', 'Deliver each product when available'), ('one', 'Deliver all products at once')], - 'Shipping Policy', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, - help="""Pick 'Deliver each product when available' if you allow partial delivery."""), - 'order_policy': fields.selection([ - ('manual', 'On Demand'), - ('picking', 'On Delivery Order'), - ('prepaid', 'Before Delivery'), - ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, - help="""On demand: A draft invoice can be created from the sales order when needed. \nOn delivery order: A draft invoice can be created from the delivery order when the products have been delivered. \nBefore delivery: A draft invoice is created from the sales order and must be paid before the products can be delivered."""), - 'shipped': fields.function(_get_shipped, string='Delivered', type='boolean', store={ - 'procurement.order': (_get_orders_procurements, ['state'], 10) - }), - 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}), - 'picking_ids': fields.function(_get_picking_ids, method=True, type='one2many', relation='stock.picking', string='Picking associated to this sale', multi='_get_picking_ids'), - 'delivery_count': fields.function(_get_picking_ids, type='integer', string='Delivery Orders', multi='_get_picking_ids'), - } - _defaults = { - 'warehouse_id': _get_default_warehouse, - 'picking_policy': 'direct', - 'order_policy': 'manual', - } - def onchange_warehouse_id(self, cr, uid, ids, warehouse_id, context=None): - val = {} - if warehouse_id: - warehouse = self.pool.get('stock.warehouse').browse(cr, uid, warehouse_id, context=context) - if warehouse.company_id: - val['company_id'] = warehouse.company_id.id - return {'value': val} - - def action_view_delivery(self, cr, uid, ids, context=None): + @api.multi + def action_view_delivery(self): ''' This function returns an action that display existing delivery orders of given sales order ids. It can either be a in a list or in a form view, if there is only one delivery order to show. ''' - - mod_obj = self.pool.get('ir.model.data') - act_obj = self.pool.get('ir.actions.act_window') - - result = mod_obj.get_object_reference(cr, uid, 'stock', 'action_picking_tree_all') - id = result and result[1] or False - result = act_obj.read(cr, uid, [id], context=context)[0] + action = self.env.ref('stock.action_picking_tree_all') + + result = { + 'name': action.name, + 'help': action.help, + 'type': action.type, + 'view_type': action.view_type, + 'view_mode': action.view_mode, + 'target': action.target, + 'context': action.context, + 'res_model': action.res_model, + } - #compute the number of delivery orders to display - pick_ids = [] - for so in self.browse(cr, uid, ids, context=context): - pick_ids += [picking.id for picking in so.picking_ids] + pick_ids = sum([order.picking_ids.ids for order in self], []) - #choose the view_mode accordingly if len(pick_ids) > 1: - result['domain'] = "[('id','in',[" + ','.join(map(str, pick_ids)) + "])]" - else: - res = mod_obj.get_object_reference(cr, uid, 'stock', 'view_picking_form') - result['views'] = [(res and res[1] or False, 'form')] - result['res_id'] = pick_ids and pick_ids[0] or False + result['domain'] = "[('id','in',["+','.join(map(str, pick_ids))+"])]" + elif len(pick_ids) == 1: + form = self.env.ref('stock.view_picking_form', False) + form_id = form.id if form else False + result['views'] = [(form_id, 'form')] + result['res_id'] = pick_ids[0] return result - def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_invoice = False, context=None): - move_obj = self.pool.get("stock.move") - res = super(sale_order,self).action_invoice_create(cr, uid, ids, grouped=grouped, states=states, date_invoice = date_invoice, context=context) - for order in self.browse(cr, uid, ids, context=context): - if order.order_policy == 'picking': - for picking in order.picking_ids: - move_obj.write(cr, uid, [x.id for x in picking.move_lines], {'invoice_state': 'invoiced'}, context=context) - return res + @api.multi + def _prepare_invoice(self): + invoice_vals = super(SaleOrder, self)._prepare_invoice() + invoice_vals['incoterms_id'] = self.incoterm.id or False + return invoice_vals - def action_wait(self, cr, uid, ids, context=None): - res = super(sale_order, self).action_wait(cr, uid, ids, context=context) - for o in self.browse(cr, uid, ids): - noprod = self.test_no_product(cr, uid, o, context) - if noprod and o.order_policy=='picking': - self.write(cr, uid, [o.id], {'order_policy': 'manual'}, context=context) + @api.model + def _prepare_procurement_group(self): + res = super(SaleOrder, self)._prepare_procurement_group() + res.update({'move_type': self.picking_policy, 'partner_id': self.partner_shipping_id.id}) return res - def _get_date_planned(self, cr, uid, order, line, start_date, context=None): - date_planned = super(sale_order, self)._get_date_planned(cr, uid, order, line, start_date, context=context) - date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT) - return date_planned - def _prepare_procurement_group(self, cr, uid, order, context=None): - res = super(sale_order, self)._prepare_procurement_group(cr, uid, order, context=None) - res.update({'move_type': order.picking_policy}) - return res - - def action_ship_end(self, cr, uid, ids, context=None): - super(sale_order, self).action_ship_end(cr, uid, ids, context=context) - for order in self.browse(cr, uid, ids, context=context): - val = {'shipped': True} - if order.state == 'shipping_except': - val['state'] = 'progress' - if (order.order_policy == 'manual'): - for line in order.order_line: - if (not line.invoiced) and (line.state not in ('cancel', 'draft')): - val['state'] = 'manual' - break - res = self.write(cr, uid, [order.id], val) - return True - - def has_stockable_products(self, cr, uid, ids, *args): - for order in self.browse(cr, uid, ids): - for order_line in order.order_line: - if order_line.state == 'cancel': - continue - if order_line.product_id and order_line.product_id.type in ('product', 'consu'): - return True - return False - - -class product_product(osv.osv): - _inherit = 'product.product' - - def need_procurement(self, cr, uid, ids, context=None): - #when sale/product is installed alone, there is no need to create procurements, but with sale_stock - #we must create a procurement for each product that is not a service. - for product in self.browse(cr, uid, ids, context=context): - if product.id and product.type in ['product', 'consu']: - return True - return super(product_product, self).need_procurement(cr, uid, ids, context=context) - -class sale_order_line(osv.osv): +class SaleOrderLine(models.Model): _inherit = 'sale.order.line' - - def _number_packages(self, cr, uid, ids, field_name, arg, context=None): - res = {} - for line in self.browse(cr, uid, ids, context=context): - try: - res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty) - except: - res[line.id] = 1 - return res - - _columns = { - 'product_packaging': fields.many2one('product.packaging', 'Packaging'), - 'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'), - 'route_id': fields.many2one('stock.location.route', 'Route', domain=[('sale_selectable', '=', True)]), - 'product_tmpl_id': fields.related('product_id', 'product_tmpl_id', type='many2one', relation='product.template', string='Product Template'), - } - - _defaults = { - 'product_packaging': False, - } - - def product_packaging_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False, - partner_id=False, packaging=False, flag=False, context=None): - if not product: - return {'value': {'product_packaging': False}} - product_obj = self.pool.get('product.product') - product_uom_obj = self.pool.get('product.uom') - pack_obj = self.pool.get('product.packaging') - warning = {} - result = {} - warning_msgs = '' - if flag: - res = self.product_id_change(cr, uid, ids, pricelist=pricelist, - product=product, qty=qty, uom=uom, partner_id=partner_id, - packaging=packaging, flag=False, context=context) - warning_msgs = res.get('warning') and res['warning'].get('message', '') or '' - - products = product_obj.browse(cr, uid, product, context=context) - if not products.packaging_ids: - packaging = result['product_packaging'] = False - - if packaging: - default_uom = products.uom_id and products.uom_id.id - pack = pack_obj.browse(cr, uid, packaging, context=context) - q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom) -# qty = qty - qty % q + q - if qty and (q and not (qty % q) == 0): - barcode = pack.barcode or _('(n/a)') - qty_pack = pack.qty - type_ul = pack.ul - if not warning_msgs: - warn_msg = _("You selected a quantity of %d Units.\n" - "But it's not compatible with the selected packaging.\n" - "Here is a proposition of quantities according to the packaging:\n" - "Barcode: %s Quantity: %s Type of ul: %s") % \ - (qty, barcode, qty_pack, type_ul.name) - warning_msgs += _("Picking Information ! : ") + warn_msg + "\n\n" - warning = { - 'title': _('Configuration Error!'), - 'message': warning_msgs - } - result['product_uom_qty'] = qty - - return {'value': result, 'warning': warning} - - def _check_routing(self, cr, uid, ids, product, warehouse_id, context=None): - """ Verify the route of the product based on the warehouse - return True if the product availibility in stock does not need to be verified - """ - is_available = False - if warehouse_id: - warehouse = self.pool['stock.warehouse'].browse(cr, uid, warehouse_id, context=context) - for product_route in product.route_ids: - if warehouse.mto_pull_id and warehouse.mto_pull_id.route_id and warehouse.mto_pull_id.route_id.id == product_route.id: - is_available = True - break - else: - try: - mto_route_id = self.pool['stock.warehouse']._get_mto_route(cr, uid, context=context) - except osv.except_osv: - # if route MTO not found in ir_model_data, we treat the product as in MTS - mto_route_id = False - if mto_route_id: - for product_route in product.route_ids: - if product_route.id == mto_route_id: - is_available = True - break - return is_available - - def product_id_change_with_wh(self, cr, uid, ids, pricelist, product, qty=0, - uom=False, qty_uos=0, uos=False, name='', partner_id=False, - lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position_id=False, flag=False, warehouse_id=False, context=None): - context = context or {} - product_uom_obj = self.pool.get('product.uom') - product_obj = self.pool.get('product.product') - warning = {} - #UoM False due to hack which makes sure uom changes price, ... in product_id_change - res = self.product_id_change(cr, uid, ids, pricelist, product, qty=qty, - uom=False, qty_uos=qty_uos, uos=uos, name=name, partner_id=partner_id, - lang=lang, update_tax=update_tax, date_order=date_order, packaging=packaging, fiscal_position_id=fiscal_position_id, flag=flag, context=context) - - if not product: - res['value'].update({'product_packaging': False}) - return res - - # set product uom in context to get virtual stock in current uom - if 'product_uom' in res.get('value', {}): - # use the uom changed by super call - context = dict(context, uom=res['value']['product_uom']) - elif uom: - # fallback on selected - context = dict(context, uom=uom) - - #update of result obtained in super function - product_obj = product_obj.browse(cr, uid, product, context=context) - res['value'].update({'product_tmpl_id': product_obj.product_tmpl_id.id, 'delay': (product_obj.sale_delay or 0.0)}) - - # Calling product_packaging_change function after updating UoM - res_packing = self.product_packaging_change(cr, uid, ids, pricelist, product, qty, uom, partner_id, packaging, context=context) - res['value'].update(res_packing.get('value', {})) - warning_msgs = res_packing.get('warning') and res_packing['warning']['message'] or '' - - if product_obj.type == 'product': - #determine if the product needs further check for stock availibility - is_available = self._check_routing(cr, uid, ids, product_obj, warehouse_id, context=context) - - #check if product is available, and if not: raise a warning, but do this only for products that aren't processed in MTO - if not is_available: - uom_record = False - if uom: - uom_record = product_uom_obj.browse(cr, uid, uom, context=context) - if product_obj.uom_id.category_id.id != uom_record.category_id.id: - uom_record = False - if not uom_record: - uom_record = product_obj.uom_id - compare_qty = float_compare(product_obj.virtual_available, qty, precision_rounding=uom_record.rounding) - if compare_qty == -1: - warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \ - (qty, uom_record.name, - max(0,product_obj.virtual_available), uom_record.name, - max(0,product_obj.qty_available), uom_record.name) - warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n" - - #update of warning messages - if warning_msgs: - warning = { - 'title': _('Configuration Error!'), - 'message' : warning_msgs + product_packaging = fields.Many2one('product.packaging', string='Packaging', default=False) + route_id = fields.Many2one('stock.location.route', string='Route', domain=[('sale_selectable', '=', True)]) + product_tmpl_id = fields.Many2one('product.template', related='product_id.product_tmpl_id', string='Product Template') + procurement_ids = fields.One2many('procurement.order', 'so_line_id', string='Procurements') + + @api.multi + @api.depends('product_id') + def _compute_qty_delivered_updateable(self): + for line in self: + if line.product_id.type not in ('consu', 'product'): + return super(SaleOrderLine, self)._compute_qty_delivered_updateable() + line.qty_delivered_updateable = False + + @api.onchange('product_id') + def _onchange_product_id_set_customer_lead(self): + self.customer_lead = self.product_id.sale_delay + return {} + + @api.onchange('product_packaging') + def _onchange_product_packaging(self): + if self.product_packaging: + return self._check_package() + return {} + + @api.onchange('product_id', 'product_uom_qty') + def _onchange_product_id_check_availability(self): + if not self.product_id: + self.product_packaging = False + return {} + self.product_tmpl_id = self.product_id.product_tmpl_id + if self.product_id.type == 'product': + product = self.product_id.with_context( + lang=self.order_id.partner_id.lang, + partner_id=self.order_id.partner_id.id, + date_order=self.order_id.date_order, + pricelist_id=self.order_id.pricelist_id.id, + uom=self.product_uom.id, + warehouse_id=self.order_id.warehouse_id.id + ) + if float_compare(product.virtual_available, self.product_uom_qty, precision_rounding=self.product_uom.rounding) == -1: + # Check if MTO, Cross-Dock or Drop-Shipping + is_available = False + for route in self.route_id+self.product_id.route_ids: + for pull in route.pull_ids: + if pull.location_id.id == self.order_id.warehouse_id.lot_stock_id.id: + is_available = True + if not is_available: + return { + 'title': _('Not enough inventory!'), + 'message' : _('You plan to sell %.2f %s but you only have %.2f %s available!\nThe stock on hand is %.2f %s.') % \ + (self.product_uom_qty, self.product_uom.name, product.virtual_available, self.product_uom.name, product.qty_available, self.product_uom.name) } - res.update({'warning': warning}) - return res + return {} + + @api.multi + def _prepare_order_line_procurement(self, group_id=False): + vals = super(SaleOrderLine, self)._prepare_order_line_procurement(group_id=group_id) + date_planned = datetime.strptime(self.order_id.date_order, DEFAULT_SERVER_DATETIME_FORMAT)\ + + timedelta(days=self.customer_lead or 0.0) - timedelta(days=self.order_id.company_id.security_lead) + vals.update({ + 'date_planned': date_planned.strftime(DEFAULT_SERVER_DATETIME_FORMAT), + 'location_id': self.order_id.partner_shipping_id.property_stock_customer.id, + 'route_ids': self.route_id and [(4, self.route_id.id)] or [], + 'warehouse_id': self.order_id.warehouse_id and self.order_id.warehouse_id.id or False, + 'partner_dest_id': self.order_id.partner_shipping_id.id, + 'so_line_id': self.id, + }) + return vals - def button_cancel(self, cr, uid, ids, context=None): - lines = self.browse(cr, uid, ids, context=context) - for procurement in lines.mapped('procurement_ids'): - for move in procurement.move_ids: - if move.state == 'done' and not move.scrapped: - raise osv.except_osv(_('Invalid Action!'), _('You cannot cancel a sale order line which is linked to a stock move already done.')) - return super(sale_order_line, self).button_cancel(cr, uid, ids, context=context) - -class stock_move(osv.osv): - _inherit = 'stock.move' - - def _get_master_data(self, cr, uid, move, inv_type, context=None): - if inv_type in ('out_invoice', 'out_refund') and (move.procurement_id.sale_line_id or move.origin_returned_move_id.procurement_id.sale_line_id): - sale_order = move.procurement_id.sale_line_id.order_id or move.origin_returned_move_id.procurement_id.sale_line_id.order_id - return sale_order.partner_invoice_id, sale_order.user_id.id, sale_order.pricelist_id.currency_id.id - elif move.picking_id.sale_id or move.origin_returned_move_id.picking_id.sale_id and inv_type in ('out_invoice', 'out_refund'): - # In case of extra move, it is better to use the same data as the original moves - sale_order = move.picking_id.sale_id or move.origin_returned_move_id.picking_id.sale_id - return sale_order.partner_invoice_id, sale_order.user_id.id, sale_order.pricelist_id.currency_id.id - return super(stock_move, self)._get_master_data(cr, uid, move, inv_type, context=context) - - def _get_invoice_line_vals(self, cr, uid, move, partner, inv_type, context=None): - res = super(stock_move, self)._get_invoice_line_vals(cr, uid, move, partner, inv_type, context=context) - if inv_type in ('out_invoice', 'out_refund') and (move.procurement_id.sale_line_id or move.origin_returned_move_id.procurement_id.sale_line_id): - sale_line = move.procurement_id.sale_line_id or move.origin_returned_move_id.procurement_id.sale_line_id - res['invoice_line_tax_ids'] = [(6, 0, [x.id for x in sale_line.tax_id])] - res['account_analytic_id'] = sale_line.order_id.project_id and sale_line.order_id.project_id.id or False - res['discount'] = sale_line.discount - if move.product_id.id != sale_line.product_id.id: - res['price_unit'] = self.pool['product.pricelist'].price_get( - cr, uid, [sale_line.order_id.pricelist_id.id], - move.product_id.id, move.product_uom_qty or 1.0, - sale_line.order_id.partner_id, context=context)[sale_line.order_id.pricelist_id.id] - else: - res['price_unit'] = sale_line.price_unit - uos_coeff = move.product_uom_qty and move.product_uos_qty / move.product_uom_qty or 1.0 - res['price_unit'] = res['price_unit'] / uos_coeff - res['sale_line_ids'] = [(4, sale_line.id)] - return res + @api.multi + def _get_delivered_qty(self): + self.ensure_one() + super(SaleOrderLine, self)._get_delivered_qty() + qty = 0.0 + for move in self.procurement_ids.mapped('move_ids').filtered(lambda r: r.state == 'done' and not r.scrapped): + if move.location_dest_id.usage == "customer": + qty += self.env['product.uom']._compute_qty_obj(move.product_uom, move.product_uom_qty, self.product_uom) + elif move.location_id.usage == "customer": + qty -= self.env['product.uom']._compute_qty_obj(move.product_uom, move.product_uom_qty, self.product_uom) + return qty + + @api.multi + def _check_package(self): + default_uom = self.product_id.product_uom + pack = self.product_packaging + qty = self.product_uom_qty + q = self.product_id.product_uom._compute_qty(pack.qty, default_uom) + if qty and q and (qty % q): + newqty = qty - (qty % q) + q + return { + 'title': _('Warning!'), + 'message': _("This product is packaged by %d %s. You should sell %d %s.") % (pack.qty, default_uom, newqty, default_uom) + } + return {} - def _get_moves_taxes(self, cr, uid, moves, inv_type, context=None): - is_extra_move, extra_move_tax = super(stock_move, self)._get_moves_taxes(cr, uid, moves, inv_type, context=context) - if inv_type == 'out_invoice': - for move in moves: - if move.procurement_id and move.procurement_id.sale_line_id: - is_extra_move[move.id] = False - extra_move_tax[move.picking_id, move.product_id] = [(6, 0, [x.id for x in move.procurement_id.sale_line_id.tax_id])] - elif move.picking_id.sale_id and move.product_id.product_tmpl_id.taxes_id: - fp = move.picking_id.sale_id.fiscal_position - res = self.pool.get("account.invoice.line").product_id_change(cr, uid, [], move.product_id.id, None, partner_id=move.picking_id.partner_id.id, fposition_id=(fp and fp.id), context=context) - extra_move_tax[0, move.product_id] = [(6, 0, res['value']['invoice_line_tax_ids'])] - else: - extra_move_tax[0, move.product_id] = [(6, 0, [x.id for x in move.product_id.product_tmpl_id.taxes_id])] - return (is_extra_move, extra_move_tax) - - def _get_taxes(self, cr, uid, move, context=None): - if move.procurement_id.sale_line_id.tax_id: - return [tax.id for tax in move.procurement_id.sale_line_id.tax_id] - return super(stock_move, self)._get_taxes(cr, uid, move, context=context) - -class stock_location_route(osv.osv): - _inherit = "stock.location.route" - _columns = { - 'sale_selectable': fields.boolean("Selectable on Sales Order Line") - } +class StockLocationRoute(models.Model): + _inherit = "stock.location.route" -class stock_picking(osv.osv): - _inherit = "stock.picking" - - def _get_partner_to_invoice(self, cr, uid, picking, context=None): - """ Inherit the original function of the 'stock' module - We select the partner of the sales order as the partner of the customer invoice - """ - if picking.sale_id: - saleorder_ids = self.pool['sale.order'].search(cr, uid, [('procurement_group_id' ,'=', picking.group_id.id)], context=context) - saleorders = self.pool['sale.order'].browse(cr, uid, saleorder_ids, context=context) - if saleorders and saleorders[0] and saleorders[0].order_policy == 'picking': - saleorder = saleorders[0] - return saleorder.partner_invoice_id.id - return super(stock_picking, self)._get_partner_to_invoice(cr, uid, picking, context=context) - - def _get_sale_id(self, cr, uid, ids, name, args, context=None): - sale_obj = self.pool.get("sale.order") - res = {} - for picking in self.browse(cr, uid, ids, context=context): - res[picking.id] = False - if picking.group_id: - sale_ids = sale_obj.search(cr, uid, [('procurement_group_id', '=', picking.group_id.id)], context=context) - if sale_ids: - res[picking.id] = sale_ids[0] - return res - - _columns = { - 'sale_id': fields.function(_get_sale_id, type="many2one", relation="sale.order", string="Sale Order"), - } - - def _get_invoice_vals(self, cr, uid, key, inv_type, journal_id, moves, context=None): - inv_vals = super(stock_picking, self)._get_invoice_vals(cr, uid, key, inv_type, journal_id, moves, context=context) - if inv_type in ('out_invoice', 'out_refund'): - sales = [x.picking_id.sale_id or x.origin_returned_move_id.picking_id.sale_id for x in moves if x.picking_id.sale_id or x.origin_returned_move_id.picking_id.sale_id] - if sales: - sale = sales[0] - inv_vals.update({ - 'fiscal_position_id': sale.fiscal_position_id.id, - 'payment_term_id': sale.payment_term_id.id, - 'user_id': sale.user_id.id, - 'team_id': sale.team_id.id, - 'name': sale.client_order_ref or '', - 'sale_ids': [(6, 0, list(set([x.id for x in sales])))], - }) - return inv_vals - - def get_service_line_vals(self, cr, uid, moves, partner, inv_type, context=None): - res = super(stock_picking, self).get_service_line_vals(cr, uid, moves, partner, inv_type, context=context) - if inv_type == 'out_invoice': - sale_line_obj = self.pool.get('sale.order.line') - orders = list(set([x.procurement_id.sale_line_id.order_id.id for x in moves if x.procurement_id.sale_line_id])) - sale_line_ids = sale_line_obj.search(cr, uid, [('order_id', 'in', orders), ('invoiced', '=', False), '|', ('product_id', '=', False), - ('product_id.type', '=', 'service')], context=context) - if sale_line_ids: - created_lines = sale_line_obj.invoice_line_create(cr, uid, sale_line_ids, context=context) - res += [(4, x) for x in created_lines] - return res + sale_selectable = fields.Boolean(string="Selectable on Sales Order Line") -class account_invoice(osv.Model): +class AccountInvoice(models.Model): _inherit = 'account.invoice' - _columns = { - 'incoterms_id': fields.many2one( - 'stock.incoterms', - "Incoterms", - help="Incoterms are series of sales terms. They are used to divide transaction costs and responsibilities between buyer and seller and reflect state-of-the-art transportation practices.", - readonly=True, - states={'draft': [('readonly', False)]}), - } - -class sale_advance_payment_inv(osv.TransientModel): - _inherit = 'sale.advance.payment.inv' - - def _prepare_advance_invoice_vals(self, cr, uid, ids, context=None): - result = super(sale_advance_payment_inv,self)._prepare_advance_invoice_vals(cr, uid, ids, context=context) - if context is None: - context = {} - - sale_obj = self.pool.get('sale.order') - sale_ids = context.get('active_ids', []) - res = [] - for sale in sale_obj.browse(cr, uid, sale_ids, context=context): - elem = filter(lambda t: t[0] == sale.id, result)[0] - elem[1]['incoterms_id'] = sale.incoterm.id or False - res.append(elem) - return res + + incoterms_id = fields.Many2one('stock.incoterms', string="Incoterms", + help="Incoterms are series of sales terms. They are used to divide transaction costs and responsibilities between buyer and seller and reflect state-of-the-art transportation practices.", + readonly=True, states={'draft': [('readonly', False)]}) -class procurement_order(osv.osv): +class ProcurementOrder(models.Model): _inherit = "procurement.order" - def _run_move_create(self, cr, uid, procurement, context=None): - vals = super(procurement_order, self)._run_move_create(cr, uid, procurement, context=context) - #copy the sequence from the sale order line on the stock move - if procurement.sale_line_id: - vals.update({'sequence': procurement.sale_line_id.sequence}) + so_line_id = fields.Many2one('sale.order.line', string='Sale Order Line') + + @api.model + def _run_move_create(self, procurement): + vals = super(ProcurementOrder, self)._run_move_create(procurement) + if self.sale_line_id: + vals.update({'sequence': self.sale_line_id.sequence}) return vals + + +class StockMove(models.Model): + _inherit = "stock.move" + + @api.multi + def action_done(self): + result = super(StockMove, self).action_done() + + # Update delivered quantities on sale order lines + todo = self.env['sale.order.line'] + for move in self: + if (move.procurement_id.so_line_id) and (move.product_id.invoice_policy in ('order', 'delivery')): + todo |= move.procurement_id.so_line_id + for line in todo: + line.qty_delivered = line._get_delivered_qty() + return result diff --git a/addons/sale_stock/sale_stock_demo.xml b/addons/sale_stock/sale_stock_demo.xml index a8b9d984c450..06010a5a6b6a 100644 --- a/addons/sale_stock/sale_stock_demo.xml +++ b/addons/sale_stock/sale_stock_demo.xml @@ -4,7 +4,6 @@ <record id="sale.sale_order_1" model="sale.order"> <field name="warehouse_id" ref="stock.warehouse0"/> - <field name="order_policy">prepaid</field> </record> <record id="sale.sale_order_2" model="sale.order"> @@ -17,23 +16,15 @@ <record id="sale.sale_order_5" model="sale.order"> <field name="warehouse_id" ref="stock.warehouse0"/> - <field name="order_policy">picking</field> </record> <record id="sale.sale_order_6" model="sale.order"> <field name="warehouse_id" ref="stock.warehouse0"/> - <field name="order_policy">picking</field> </record> - + <record id="sale.sale_order_8" model="sale.order"> <field name="warehouse_id" ref="stock.warehouse0"/> </record> - - <!-- Confirm some Sale Orders--> - <workflow action="order_confirm" model="sale.order" ref="sale.sale_order_5"/> - - <!-- Run all schedulers --> - <function model="procurement.order" name="run_scheduler"/> - + </data> </openerp> diff --git a/addons/sale_stock/sale_stock_demo.yml b/addons/sale_stock/sale_stock_demo.yml index 5ffa75f0f780..66df6e9643aa 100644 --- a/addons/sale_stock/sale_stock_demo.yml +++ b/addons/sale_stock/sale_stock_demo.yml @@ -6,7 +6,6 @@ if account_id: vals = { 'warehouse_id': ref('stock.warehouse0'), - 'order_policy': 'prepaid', } self._update(cr, uid, 'sale.order', 'sale', vals, 'sale_order_4') diff --git a/addons/sale_stock/sale_stock_view.xml b/addons/sale_stock/sale_stock_view.xml index 3f72d3f1c102..f67aa533dbc9 100644 --- a/addons/sale_stock/sale_stock_view.xml +++ b/addons/sale_stock/sale_stock_view.xml @@ -2,16 +2,12 @@ <openerp> <data> - <record id="view_order_form_inherit" model="ir.ui.view"> + <record id="view_order_form_inherit_sale_stock" model="ir.ui.view"> <field name="name">sale.order.form.sale.stock</field> <field name="model">sale.order</field> <field name="inherit_id" ref="sale.view_order_form"/> <field name="arch" type="xml"> <data> - <xpath expr="//button[@name='invoice_corrected']" position="after"> - <button name="ship_recreate" states="shipping_except" string="Recreate Delivery Order"/> - <button name="ship_corrected" states="shipping_except" string="Ignore Exception"/> - </xpath> <xpath expr="//button[@name='action_view_invoice']" position="before"> <field name="picking_ids" invisible="1"/> <button type="object" @@ -19,140 +15,31 @@ class="oe_stat_button" icon="fa-truck" attrs="{'invisible': [('delivery_count', '=', 0)]}" groups="base.group_user"> - <field name="delivery_count" widget="statinfo" string="Transfers" help="Delivery Orders"/> + <field name="delivery_count" widget="statinfo" string="Delivery"/> </button> </xpath> - <xpath expr="//button[@name='action_cancel']" position="after"> - <button name="ship_cancel" states="shipping_except" string="Cancel Order"/> - </xpath> - <field name="state" position="attributes"> - <attribute name="statusbar_colors" t-translate="off">{"shipping_except":"red","invoice_except":"red","waiting_date":"blue"}</attribute> - </field> - <field name="company_id" position="replace"> - <field name="company_id" readonly="True"/> - </field> <xpath expr="//group[@name='sales_person']" position="before"> <group string="Shipping Information" name="sale_shipping"> - <field name="warehouse_id" on_change="onchange_warehouse_id(warehouse_id)" options="{'no_create': True}" groups="stock.group_locations"/> + <field name="warehouse_id" options="{'no_create': True}" groups="stock.group_locations"/> <field name="incoterm" widget="selection" groups="base.group_user"/> <field name="picking_policy" required="True"/> </group> </xpath> - <xpath expr="//field[@name='order_line']/form//field[@name='product_id']" position="attributes"> - <!-- no product_uos to force reset of product_uom, product_uos and product_uos_qty in porduct_id_change --> - <attribute name="on_change">product_id_change_with_wh( - parent.pricelist_id, product_id, product_uom_qty, False, product_uos_qty, False, name, - parent.partner_id, False, True, parent.date_order, product_packaging, parent.fiscal_position_id, - False, parent.warehouse_id, context) - </attribute> - <attribute name="context">{'partner_id':parent.partner_id, 'quantity':product_uom_qty, 'pricelist':parent.pricelist_id, 'uom':False}</attribute> - </xpath> - <xpath expr="//field[@name='order_line']/tree//field[@name='product_id']" position="attributes"> - <attribute name="on_change">product_id_change_with_wh( - parent.pricelist_id, product_id, product_uom_qty, False, product_uos_qty, False, name, - parent.partner_id, False, True, parent.date_order, product_packaging, parent.fiscal_position_id, - False, parent.warehouse_id, context) - </attribute> - </xpath> - <xpath expr="//field[@name='order_line']/form//field[@name='product_uos_qty']" position="attributes"> - <!-- keep product_uos to force update of product_uom and product_uom_qty in porduct_id_change --> - <attribute name="on_change">product_id_change_with_wh( - parent.pricelist_id, product_id, product_uom_qty, False, product_uos_qty, product_uos, name, - parent.partner_id, False, True, parent.date_order, product_packaging, parent.fiscal_position_id, - False, parent.warehouse_id, context) - </attribute> + <xpath expr="//page/field[@name='order_line']/form/group/group/field[@name='tax_id']" position="before"> + <field name="product_tmpl_id" invisible="1"/> + <field name="product_packaging" context="{'default_product_tmpl_id': product_tmpl_id, 'partner_id':parent.partner_id, 'quantity':product_uom_qty, 'pricelist':parent.pricelist_id, 'uom':product_uom}" domain="[('product_tmpl_id','=',product_tmpl_id)]" groups="product.group_stock_packaging" /> </xpath> - <xpath expr="//field[@name='order_line']/form//field[@name='product_uom_qty']" position="attributes"> - <attribute name="on_change">product_id_change_with_wh( - parent.pricelist_id, product_id, product_uom_qty, product_uom, product_uos_qty, False, name, - parent.partner_id, False, False, parent.date_order, product_packaging, parent.fiscal_position_id, - True, parent.warehouse_id, context) - </attribute> + <xpath expr="//field[@name='order_line']/form/group/group/field[@name='price_unit']" position="before"> + <field name="route_id" groups="sale_stock.group_route_so_lines"/> </xpath> - <xpath expr="//group[@name='technical']" position="inside"> - <field name="shipped" groups="base.group_no_one"/> - </xpath> - <xpath expr="//page/field[@name='order_line']/form/group/group/field[@name='tax_id']" position="after"> - <label for="delay"/> - <div> - <field name="delay" class="oe_inline"/> days - </div> - </xpath> - <xpath expr="//page/field[@name='order_line']/form/group/group/field[@name='tax_id']" position="before"> - <field name="product_tmpl_id" invisible="1"/> - <field name="product_packaging" context="{'default_product_tmpl_id': product_tmpl_id, 'partner_id':parent.partner_id, 'quantity':product_uom_qty, 'pricelist':parent.pricelist_id, 'uom':product_uom}" on_change="product_packaging_change(parent.pricelist_id, product_id, product_uom_qty, product_uom, parent.partner_id, product_packaging, True, context)" domain="[('product_tmpl_id','=',product_tmpl_id)]" groups="product.group_stock_packaging" /> - </xpath> - <xpath expr="//page/field[@name='order_line']/tree/field[@name='sequence']" position="after"> - <field name="delay" invisible="1"/> - </xpath> - <xpath expr="//page/field[@name='order_line']/tree/field[@name='th_weight']" position="after"> - <field name="product_packaging" invisible="1"/> - </xpath> - <xpath expr="//group[@name='sale_pay']" position="inside"> - <field name="order_policy"/> - </xpath> - <xpath expr="//field[@name='order_line']/form/group/group/field[@name='price_unit']" position="before"> + <xpath expr="//field[@name='order_line']/tree/field[@name='price_unit']" position="before"> <field name="route_id" groups="sale_stock.group_route_so_lines"/> - </xpath> - <xpath expr="//field[@name='order_line']/tree/field[@name='price_unit']" position="before"> - <field name="route_id" groups="sale_stock.group_route_so_lines"/> - </xpath> + </xpath> </data> </field> </record> - <record id="view_res_partner_tree_type" model="ir.ui.view"> - <field name="name">res.partner.tree.inherit.type</field> - <field name="model">res.partner</field> - <field name="inherit_id" ref="base.view_partner_tree"/> - <field name="arch" type="xml"> - <field name="parent_id" position="after"> - <field name="type" invisible="context.get('hide_type', 1)"/> - </field> - </field> - </record> - - <!-- On the customer/supplier form if "Allow a different address for - delivery and invoicing" is set add "Contact Details" in the more menu - showing the list of contact with their types --> - <act_window - id="res_partner_rule_children" - name="Contact Details" - context="{'default_parent_id': active_id, 'hide_type': 0}" - domain="[('parent_id','=',active_id)]" - res_model="res.partner" - src_model="res.partner" - view_mode="tree,form,kanban" - view_type="form" - groups="sale.group_delivery_invoice_address" - /> - - <record id="view_picking_internal_search_inherit" model="ir.ui.view"> - <field name="name">stock.picking.search.inherit</field> - <field name="model">stock.picking</field> - <field name="inherit_id" ref="stock.view_picking_internal_search"/> - <field name="arch" type="xml"> - <xpath expr="//field[@name='partner_id']" position="before"> - <filter string="To Invoice" name="to_invoice" domain="[('invoice_state', '=', '2binvoiced')]"/> - </xpath> - </field> - </record> - - - <record id="view_order_form_inherit2" model="ir.ui.view"> - <field name="name">sale.order.line.form.sale.stock.location</field> - <field name="model">sale.order.line</field> - <field name="inherit_id" ref="sale.view_order_line_form2"/> - <field name="arch" type="xml"> - <data> - <xpath expr="//field[@name='price_unit']" position="before"> - <field name="route_id" groups="sale_stock.group_route_so_lines" /> - </xpath> - </data> - </field> - </record> - - <record id="view_order_line_tree_inherit" model="ir.ui.view"> + <record id="view_order_line_tree_inherit_sale_stock" model="ir.ui.view"> <field name="name">sale.order.line.tree.sale.stock.location</field> <field name="inherit_id" ref="sale.view_order_line_tree"/> <field name="model">sale.order.line</field> @@ -163,7 +50,7 @@ </field> </record> - <template id="report_sale_order_incoterm" inherit_id="sale.report_saleorder_document"> + <template id="report_saleorder_document_inherit_sale_stock" inherit_id="sale.report_saleorder_document"> <xpath expr="//div[@name='payment_term']" position="after"> <div class="col-xs-3" t-if="doc.incoterm" groups="sale.group_display_incoterm"> <strong>Incoterms:</strong> diff --git a/addons/sale_stock/sale_stock_workflow.xml b/addons/sale_stock/sale_stock_workflow.xml deleted file mode 100644 index 308167500350..000000000000 --- a/addons/sale_stock/sale_stock_workflow.xml +++ /dev/null @@ -1,6 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<openerp> - <data> - - </data> -</openerp> diff --git a/addons/sale_stock/stock_view.xml b/addons/sale_stock/stock_view.xml index 24c6d46caabe..95f72bd6f986 100644 --- a/addons/sale_stock/stock_view.xml +++ b/addons/sale_stock/stock_view.xml @@ -3,7 +3,7 @@ <data> <!-- Add menu: Billing - Deliveries to invoice --> - <record id="outgoing_picking_list_to_invoice" model="ir.actions.act_window"> + <record id="stock_picking_action_outgoing_picking_list_to_invoice" model="ir.actions.act_window"> <field name="name">Deliveries to Invoice</field> <field name="res_model">stock.picking</field> <field name="type">ir.actions.act_window</field> @@ -18,9 +18,9 @@ groups="base.group_sale_salesman" parent="base.menu_base_partner" sequence="5" /> <menuitem id="base.menu_invoiced" name="Invoicing" parent="base.menu_aftersale" sequence="1"/> - <menuitem action="outgoing_picking_list_to_invoice" id="menu_action_picking_list_to_invoice" parent="base.menu_invoiced" sequence="20"/> + <menuitem action="stock_picking_action_outgoing_picking_list_to_invoice" id="menu_action_picking_list_to_invoice" parent="base.menu_invoiced" sequence="20"/> - <record id="stock_location_route_form_view_inherit" model="ir.ui.view"> + <record id="stock_location_route_form_view_inherit_sale_stock" model="ir.ui.view"> <field name="name">stock.location.route.form</field> <field name="inherit_id" ref="stock.stock_location_route_form_view"/> <field name="model">stock.location.route</field> diff --git a/addons/sale_stock/test/cancel_order_sale_stock.yml b/addons/sale_stock/test/cancel_order_sale_stock.yml deleted file mode 100644 index 49835b064c92..000000000000 --- a/addons/sale_stock/test/cancel_order_sale_stock.yml +++ /dev/null @@ -1,94 +0,0 @@ -- - In order to test the cancel sale order with that user which have salesman rights. - First I confirm order. -- - !context - uid: 'res_sale_stock_salesman' -- - !workflow {model: sale.order, action: order_confirm, ref: sale.sale_order_8} -- - I do a partial delivery order as a stock user. -- - !context - uid: 'res_stock_user' -- - !python {model: stock.picking}: | - domain = [('origin','=','Test/001')] - picks = self.search(cr, uid, domain, context=context) - pick = self.browse(cr, uid, picks[-1], context=context) - self.pool.get('stock.pack.operation').create(cr, uid, { - 'picking_id': pick.id, - 'product_id': ref('product.product_product_27'), - 'product_uom_id': ref('product.product_uom_unit'), - 'product_qty': 1, - 'location_id': pick.location_id.id, - 'location_dest_id': pick.location_dest_id.id, - }) - pick.do_transfer() -- - I test that I have two pickings, one done and one backorder to do -- - !python {model: stock.picking}: | - picks = self.search(cr, uid, [('origin','=','Test/001')]) - assert len(picks)>1, 'Only one picking, partial picking may have failed!' - picks = self.search(cr, uid, [('origin','=','Test/001'), ('state','=','done')]) - assert len(picks)==1, 'You should have one delivery order which is done!' - picks = self.search(cr, uid, [('origin','=','Test/001'), ('backorder_id','=',picks[0])]) - assert len(picks)==1, 'You should have one backorder to process!' -- - I cancel the backorder -- - !python {model: stock.picking}: | - picks = self.search(cr, uid, [('origin','=','Test/001'),('backorder_id','<>',False)]) - self.action_cancel(cr, uid, picks) -- - I run the scheduler. -- - !python {model: procurement.order}: | - - self.run_scheduler(cr, uid) -- - Salesman can also check order therefore test with that user which have salesman rights, -- - !context - uid: 'res_sale_stock_salesman' -- - I check order status in "Ship Exception". -- - !assert {model: sale.order, id: sale.sale_order_8, string: Sale order should be in shipping exception}: - - state == "shipping_except" -- - Now I regenerate shipment. -- - !workflow {model: sale.order, action: ship_recreate, ref: sale.sale_order_8} -- - I check state of order in 'To Invoice'. -- - !assert {model: sale.order, id: sale.sale_order_8, string: Sale order should be In Progress state}: - - state == 'manual' -- - I make invoice for order. -- - !workflow {model: sale.order, action: manual_invoice, ref: sale.sale_order_8} -- - To cancel the sale order from Invoice Exception, I have to cancel the invoice of sale order. -- - !python {model: sale.order}: | - invoice_ids = self.browse(cr, uid, ref("sale.sale_order_8")).invoice_ids - first_invoice_id = invoice_ids[0] - self.pool.get('account.invoice').signal_workflow(cr, uid, [first_invoice_id.id], 'invoice_cancel') -- - I check order status in "Invoice Exception" and related invoice is in cancel state. -- - !assert {model: sale.order, id: sale.sale_order_8, string: Sale order should be in Invoice Exception state}: - - state == "invoice_except", "Order should be in Invoice Exception state after cancel Invoice" -- - Then I click on the Ignore Exception button. -- - !workflow {model: sale.order, action: invoice_corrected, ref: sale.sale_order_8} - -- - I check state of order in 'In Progress'. -- - !assert {model: sale.order, id: sale.sale_order_8, string: Sale order should be In progress state}: - - state == 'progress' diff --git a/addons/sale_stock/test/picking_order_policy.yml b/addons/sale_stock/test/picking_order_policy.yml deleted file mode 100644 index 097de63919ef..000000000000 --- a/addons/sale_stock/test/picking_order_policy.yml +++ /dev/null @@ -1,217 +0,0 @@ -- - In order to test process of the Sale Order with access rights of saleman, -- - !context - uid: 'res_sale_stock_salesman' -- - Create a new SO to be sure we don't have one with product that can explode in mrp -- - !record {model: sale.order, id: sale_order_service}: - partner_id: base.res_partner_18 - partner_invoice_id: base.res_partner_18 - partner_shipping_id: base.res_partner_18 - user_id: base.user_root - pricelist_id: product.list0 - warehouse_id: stock.warehouse0 - order_policy: picking -- - Add SO line with service type product in SO to check flow which contain service type product in SO(BUG#1167330). -- - !record {model: sale.order.line, id: sale_order_1}: - name: 'On Site Assistance' - product_id: product.product_product_2 - product_uom_qty: 1.0 - product_uom: 1 - price_unit: 150.0 - order_id: sale_order_service -- - Add a second SO line with a normal product -- - !record {model: sale.order.line, id: sale_order_2}: - name: 'Mouse Optical' - product_id: product.product_product_10 - product_uom_qty: 1.0 - product_uom: 1 - price_unit: 150.0 - order_id: sale_order_service -- - First I check the total amount of the Quotation before Approved. -- - !python {model: sale.order}: | - from openerp.tools import float_compare - so = self.browse(cr, uid, ref('sale_order_service')) - float_compare(sum([l.price_subtotal for l in so.order_line]), so.amount_untaxed, precision_digits=2) == 0, "The amount of the Quotation is not correctly computed" -- - I set an explicit invoicing partner that is different from the main SO Customer -- - !python {model: sale.order}: | - order = self.browse(cr, uid, ref("sale_order_service")) - order.write({'partner_invoice_id': ref('base.res_partner_address_30')}) -- - I confirm the quotation with Invoice based on deliveries policy. -- - !workflow {model: sale.order, action: order_confirm, ref: sale_order_service} -- - I check that invoice should not created before dispatch delivery. -- - !python {model: sale.order}: | - order = self.pool.get('sale.order').browse(cr, uid, ref("sale_order_service")) - assert order.state == 'progress', 'Order should be in inprogress.' - assert len(order.invoice_ids) == False, "Invoice should not created." -- - I check the details of procurement after confirmed quotation. -- - !python {model: sale.order}: | - from datetime import datetime, timedelta - from dateutil.relativedelta import relativedelta - from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT - order = self.browse(cr, uid, ref("sale_order_service")) - for order_line in order.order_line: - if order_line.product_id.type == 'product': - procurement = order_line.procurement_ids[0] - date_planned = datetime.strptime(order.date_order, DEFAULT_SERVER_DATETIME_FORMAT) + relativedelta(days=order_line.delay or 0.0) - date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT) - assert procurement.date_planned == date_planned, "Scheduled date is not correspond." - assert procurement.product_id.id == order_line.product_id.id, "Product is not correspond." - assert procurement.product_qty == order_line.product_uom_qty, "Qty is not correspond." - assert procurement.product_uom.id == order_line.product_uom.id, "UOM is not correspond." -- - Only stock user can change data related warehouse therefore test with that user which have stock user rights, -- - !context - uid: 'res_stock_user' -- - I run the scheduler. -- - !python {model: procurement.order}: | - self.run_scheduler(cr, uid) -- - Salesman can also check order therefore test with that user which have salesman rights, -- - !context - uid: 'res_sale_stock_salesman' -- - I check the details of delivery order after confirmed quotation. -- - !python {model: sale.order}: | - from datetime import datetime, timedelta - from dateutil.relativedelta import relativedelta - from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT - sale_order = self.browse(cr, uid, ref("sale_order_service")) - assert sale_order.picking_ids, "Delivery order is not created." - for picking in sale_order.picking_ids: - assert picking.state == "auto" or "confirmed", "Delivery order should be in 'Waitting Availability' state." - assert picking.origin == sale_order.name,"Origin of Delivery order is not correspond with sequence number of sale order." - assert picking.picking_type_id == self.pool.get('ir.model.data').get_object(cr, uid, 'stock', 'picking_type_out', context=context),"Shipment should be Outgoing." - assert picking.move_type == sale_order.picking_policy,"Delivery Method is not corresponding with delivery method of sale order." - assert picking.partner_id.id == sale_order.partner_shipping_id.id,"Shipping Address is not correspond with sale order." - assert picking.note == sale_order.note,"Note is not correspond with sale order." - assert picking.invoice_state == (sale_order.order_policy=='picking' and '2binvoiced') or 'none',"Invoice policy is not correspond with sale order." - assert len(picking.move_lines) == len(sale_order.order_line) - 1, "Total move of delivery order are not corresposning with total sale order lines." - location_id = sale_order.warehouse_id.lot_stock_id.id - for move in picking.move_lines: - order_line = move.procurement_id.sale_line_id - date_planned = datetime.strptime(sale_order.date_order, DEFAULT_SERVER_DATETIME_FORMAT) + relativedelta(days=order_line.delay or 0.0) - date_planned = (date_planned - timedelta(days=sale_order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT) - assert datetime.strptime(move.date_expected, DEFAULT_SERVER_DATETIME_FORMAT) == datetime.strptime(date_planned, DEFAULT_SERVER_DATETIME_FORMAT), "Excepted Date is not correspond with Planned Date." - assert move.product_id.id == order_line.product_id.id,"Product is not correspond." - assert move.product_qty == order_line.product_uom_qty,"Product Quantity is not correspond." - assert move.product_uom.id == order_line.product_uom.id,"Product UOM is not correspond." - assert move.product_uos_qty == (order_line.product_uos and order_line.product_uos_qty or order_line.product_uom_qty), "Product UOS Quantity is not correspond." - assert move.product_uos.id == (order_line.product_uos and order_line.product_uos.id or order_line.product_uom.id), "Product UOS is not correspond" - assert move.product_packaging.id == order_line.product_packaging.id,"Product packaging is not correspond." - #assert move.location_id.id == location_id,"Source Location is not correspond." -- - Now, I dispatch delivery order. -- - !python {model: stock.picking}: | - order = self.pool.get('sale.order').browse(cr, uid, ref("sale_order_service"), context=context) - for pick in order.picking_ids: - data = pick.force_assign() - if data == True: - pick.do_transfer() -- - I run the scheduler. -- - !python {model: procurement.order}: | - self.run_scheduler(cr, uid) -- - I check sale order to verify shipment. -- - !python {model: sale.order}: | - order = self.pool.get('sale.order').browse(cr, uid, ref("sale_order_service")) - assert order.shipped == True, "Sale order is not Delivered." - #assert order.state == 'progress', 'Order should be in inprogress.' - assert len(order.invoice_ids) == False, "Invoice should not created on dispatch delivery order." -- - I create Invoice from Delivery Order. -- - !python {model: stock.invoice.onshipping}: | - sale = self.pool.get('sale.order') - sale_order = sale.browse(cr, uid, ref("sale_order_service")) - ship_ids = [x.id for x in sale_order.picking_ids] - wiz_id = self.create(cr, uid, {'journal_id': ref('sales_journal')}, - {'active_ids': ship_ids, 'active_model': 'stock.picking'}) - self.create_invoice(cr, uid, [wiz_id], {"active_ids": ship_ids, "active_id": ship_ids[0]}) -- - I check the invoice details after dispatched delivery. -- - !python {model: sale.order}: | - from openerp.tools import float_compare - order = self.browse(cr, uid, ref("sale_order_service")) - assert order.invoice_ids, "Invoice is not created." - ac = order.partner_invoice_id.property_account_receivable_id.id - journal_ids = self.pool.get('account.journal').search(cr, uid, [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)]) - for invoice in order.invoice_ids: - assert invoice.type == 'out_invoice',"Invoice should be Customer Invoice." - assert invoice.account_id.id == ac,"Invoice account is not correspond." - assert invoice.reference == order.client_order_ref or order.name,"Reference is not correspond." - assert invoice.partner_id.id == order.partner_invoice_id.id,"Customer does not correspond." - assert invoice.currency_id.id == order.pricelist_id.currency_id.id, "Currency is not correspond." - assert (invoice.comment or '') == (order.note or ''),"Note is not correspond." - assert invoice.journal_id.id in journal_ids,"Sales Journal is not link on Invoice." - assert invoice.payment_term_id.id == order.payment_term_id.id, "Payment term is not correspond." - for so_line in order.order_line: - inv_line = so_line.invoice_lines[0] - ac = so_line.product_id.property_account_income_id.id or so_line.product_id.categ_id.property_account_income_categ_id.id - assert inv_line.product_id.id == so_line.product_id.id or False,"Product is not correspond" - assert inv_line.account_id.id == ac,"Account of Invoice line is not corresponding." - assert inv_line.uos_id.id == (so_line.product_uos and so_line.product_uos.id or so_line.product_uom.id), "Product UOS is not correspond." - assert float_compare(inv_line.price_unit, so_line.price_unit , precision_digits=2) == 0, "Price Unit is not correspond." - assert inv_line.quantity == (so_line.product_uos and so_line.product_uos_qty or so_line.product_uom_qty), "Product qty is not correspond." - assert inv_line.price_subtotal == so_line.price_subtotal, "Price sub total is not correspond." -- - Only Stock manager can open the Invoice therefore test with that user which have stock manager rights, -- - !context - uid: 'res_stock_manager' -- - I open the Invoice. -- - !python {model: sale.order}: | - so = self.browse(cr, uid, ref("sale_order_service")) - account_invoice_obj = self.pool.get('account.invoice') - for invoice in so.invoice_ids: - account_invoice_obj.signal_workflow(cr, uid, [invoice.id], 'invoice_open') -- - I pay the invoice -- - !python {model: account.invoice}: | - sale_order = self.pool.get('sale.order') - order = sale_order.browse(cr, uid, ref("sale_order_service")) - journal_ids = self.pool.get('account.journal').search(cr, uid, [('type', '=', 'cash'), ('company_id', '=', order.company_id.id)], limit=1) - import time - for invoice in order.invoice_ids: - self.pay_and_reconcile(cr, uid, [invoice.id], journal_ids[0]) -- - To test process of the Sale Order with access rights of saleman, -- - !context - uid: 'res_sale_stock_salesman' -- - I check the order after paid invoice. -- - !python {model: sale.order}: | - order = self.browse(cr, uid, ref("sale_order_service")) - assert order.invoiced == True, "Sale order is not invoiced." - assert order.state == 'done', 'Order should be in closed.' diff --git a/addons/sale_stock/test/prepaid_order_policy.yml b/addons/sale_stock/test/prepaid_order_policy.yml deleted file mode 100644 index 74449371adaf..000000000000 --- a/addons/sale_stock/test/prepaid_order_policy.yml +++ /dev/null @@ -1,30 +0,0 @@ -- - In order to test the Prepaid Order Policy, I create a product -- - !record {model: product.product, id: product_prepaid1}: - name: 'OpenERP Documentation Book' - list_price: 60.60 -- - Now i create a sale order that uses my new product -- - !record {model: sale.order, id: sale_order_prepaid1}: - partner_id: base.res_partner_2 - order_policy: prepaid - order_line: - - product_id: sale_stock.product_prepaid1 - product_uom_qty: 10 -- - Now I confirm the Quotation with "Pay before delivery" policy with access rights of salesman. -- - !context - uid: 'res_sale_stock_salesman' -- - !workflow {model: sale.order, action: order_confirm, ref: sale_order_prepaid1} -- - I check that delivery order should not created before invoice is paid. -- - !python {model: sale.order}: | - sale_order = self.browse(cr, uid, ref("sale_order_prepaid1")) - assert len(sale_order.picking_ids) == False, "Delivery order should not created before invoice." - assert sale_order.invoice_ids, "Invoice should be created." - diff --git a/addons/sale_stock/test/sale_order_canceled_line.yml b/addons/sale_stock/test/sale_order_canceled_line.yml deleted file mode 100644 index d78c88a09911..000000000000 --- a/addons/sale_stock/test/sale_order_canceled_line.yml +++ /dev/null @@ -1,45 +0,0 @@ -- - I create a draft Sale Order with 2 lines but 1 canceled in order to check if the canceled lines are not considered in the logic -- - !record {model: sale.order, id: sale_order_cl_3}: - partner_id: base.res_partner_3 - partner_invoice_id: base.res_partner_address_25 - partner_shipping_id: base.res_partner_address_25 - pricelist_id: product.list0 - order_policy: manual -- - !record {model: sale.order.line, id: sale_order_cl_3_line_1}: - order_id: sale_order_cl_3 - product_id: product.product_product_27 - product_uom_qty: 1 - product_uom: 1 - price_unit: 3645 - name: 'Laptop Customized' -- - !record {model: sale.order.line, id: sale_order_cl_3_line_2}: - order_id: sale_order_cl_3 - product_id: product.product_product_12 - product_uom_qty: 1 - product_uom: 1 - price_unit: 12.50 - name: 'Mouse, Wireless' -- - I cancel the first line -- - !python {model: sale.order.line, id: sale_order_cl_3_line_1}: | - self.button_cancel() -- - I confirm the sale order -- - !workflow {model: sale.order, action: order_confirm, ref: sale_order_cl_3} -- - I check that no procurement has been generated for the canceled line -- - !assert {model: sale.order.line, id: sale_order_cl_3_line_1, string: The canceled line should not have a procurement}: - - not procurement_ids -- - I check that we have only 1 stock move, for the not canceled line -- - !python {model: sale.order, id: sale_order_cl_3}: | - moves = self.picking_ids.mapped('move_lines') - assert len(moves) == 1, "We should have 1 move, got %s" % len(moves) diff --git a/addons/sale_stock/test/sale_order_onchange.yml b/addons/sale_stock/test/sale_order_onchange.yml deleted file mode 100644 index ef5c30015546..000000000000 --- a/addons/sale_stock/test/sale_order_onchange.yml +++ /dev/null @@ -1,33 +0,0 @@ -- - Only sales manager Creates product so let's check with access rights of salemanager. -- - !context - uid: 'res_sale_stock_salesmanager' -- - In order to test the onchange of the Sale Order, I create a product -- - !record {model: product.product, id: product_onchange1}: - name: 'Devil Worship Book' - lst_price: 66.6 - default_code: 'DWB00001' -- - In sale order to test process of onchange of Sale Order with access rights of saleman. -- - !context - uid: 'res_sale_stock_salesman' -- - Now i create a sale order that uses my new product -- - !record {model: sale.order, id: sale_order_onchange1}: - partner_id: base.res_partner_2 - order_line: - - product_id: sale_stock.product_onchange1 - product_uom_qty: 10 -- - I verify that the onchange of product on sale order line was correctly triggered -- - !python {model: sale.order}: | - from openerp.tools import float_compare - order_line = self.browse(cr, uid, ref('sale_order_onchange1')).order_line - assert order_line[0].name == u'[DWB00001] Devil Worship Book', "The onchange function of product was not correctly triggered" - assert float_compare(order_line[0].price_unit, 66.6, precision_digits=2) == 0, "The onchange function of product was not correctly triggered" diff --git a/addons/sale_stock/test/sale_stock_users.yml b/addons/sale_stock/test/sale_stock_users.yml deleted file mode 100644 index 559929632c9b..000000000000 --- a/addons/sale_stock/test/sale_stock_users.yml +++ /dev/null @@ -1,57 +0,0 @@ -- - Create a user as 'Stock Salesmanager' -- - !record {model: res.users, id: res_sale_stock_salesmanager, view: False}: - company_id: base.main_company - name: Stock Sales manager - login: ssm - email: ss_salesmanager@yourcompany.com -- - I added groups for Salesmanager. -- - !record {model: res.users, id: res_sale_stock_salesmanager}: - groups_id: - - base.group_sale_manager -- - Create a user as 'Stock Salesman' -- - !record {model: res.users, id: res_sale_stock_salesman, view: False}: - company_id: base.main_company - name: Stock Salesman - login: ssu - email: ss_salesman@yourcompany.com -- - I added groups for Stock Salesman. -- - !record {model: res.users, id: res_sale_stock_salesman}: - groups_id: - - base.group_sale_salesman_all_leads - - stock.group_stock_user -- - Create a user as 'Stock User' -- - !record {model: res.users, id: res_stock_user, view: False}: - company_id: base.main_company - name: Stock User - login: sau - email: stock_user@yourcompany.com -- - I added groups for Stock User. -- - !record {model: res.users, id: res_stock_user}: - groups_id: - - stock.group_stock_user -- - Create a user as 'Stock Manager' -- - !record {model: res.users, id: res_stock_manager, view: False}: - company_id: base.main_company - name: Stock Manager - login: sam - email: stock_manager@yourcompany.com -- - I added groups for Stock Manager. -- - !record {model: res.users, id: res_stock_manager}: - groups_id: - - stock.group_stock_manager diff --git a/addons/sale_stock/tests/__init__.py b/addons/sale_stock/tests/__init__.py new file mode 100644 index 000000000000..ebc608a933e1 --- /dev/null +++ b/addons/sale_stock/tests/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +import test_sale_stock diff --git a/addons/sale_stock/tests/test_sale_stock.py b/addons/sale_stock/tests/test_sale_stock.py new file mode 100644 index 000000000000..87dd85c6df37 --- /dev/null +++ b/addons/sale_stock/tests/test_sale_stock.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +from openerp.addons.sale.tests.test_sale_common import TestSale + + +class TestSaleStock(TestSale): + def test_00_sale_stock_invoice(self): + """ + Test SO's changes when playing around with stock moves, quants, pack operations, pickings + and whatever other model there is in stock with "invoice on delivery" products + """ + inv_obj = self.env['account.invoice'] + self.so = self.env['sale.order'].create({ + 'partner_id': self.partner.id, + 'partner_invoice_id': self.partner.id, + 'partner_shipping_id': self.partner.id, + 'order_line': [(0, 0, {'name': p.name, 'product_id': p.id, 'product_uom_qty': 2, 'product_uom': p.uom_id.id, 'price_unit': p.list_price}) for (_, p) in self.products.iteritems()], + 'pricelist_id': self.env.ref('product.list0').id, + 'picking_policy': 'direct', + }) + + # confirm our standard so, check the picking + self.so.action_confirm() + self.assertTrue(self.so.picking_ids, 'Sale Stock: no picking created for "invoice on delivery" stockable products') + # invoice on order + self.so.action_invoice_create() + + # deliver partially, check the so's invoice_status and delivered quantities + self.assertEqual(self.so.invoice_status, 'no', 'Sale Stock: so invoice_status should be "nothing to invoice" after invoicing') + pick = self.so.picking_ids + pick.force_assign() + pick.pack_operation_product_ids.write({'qty_done': 1}) + wiz_act = pick.do_new_transfer() + wiz = self.env[wiz_act['res_model']].browse(wiz_act['res_id']) + wiz.process() + self.assertEqual(self.so.invoice_status, 'to invoice', 'Sale Stock: so invoice_status should be "to invoice" after partial delivery') + del_qties = [sol.qty_delivered for sol in self.so.order_line] + del_qties_truth = [1.0 if sol.product_id.type in ['product', 'consu'] else 0.0 for sol in self.so.order_line] + self.assertEqual(del_qties, del_qties_truth, 'Sale Stock: delivered quantities are wrong after partial delivery') + # invoice on delivery: only stockable products + inv_id = self.so.action_invoice_create() + inv_1 = inv_obj.browse(inv_id) + self.assertTrue(all([il.product_id.invoice_policy == 'delivery' for il in inv_1.invoice_line_ids]), + 'Sale Stock: invoice should only contain "invoice on delivery" products') + + # complete the delivery and check invoice_status again + self.assertEqual(self.so.invoice_status, 'no', + 'Sale Stock: so invoice_status should be "nothing to invoice" after partial delivery and invoicing') + self.assertEqual(len(self.so.picking_ids), 2, 'Sale Stock: number of pickings should be 2') + pick_2 = self.so.picking_ids[0] + pick_2.force_assign() + pick_2.pack_operation_product_ids.write({'qty_done': 1}) + self.assertIsNone(pick_2.do_new_transfer(), 'Sale Stock: second picking should be final without need for a backorder') + self.assertEqual(self.so.invoice_status, 'to invoice', 'Sale Stock: so invoice_status should be "to invoice" after complete delivery') + del_qties = [sol.qty_delivered for sol in self.so.order_line] + del_qties_truth = [2.0 if sol.product_id.type in ['product', 'consu'] else 0.0 for sol in self.so.order_line] + self.assertEqual(del_qties, del_qties_truth, 'Sale Stock: delivered quantities are wrong after complete delivery') + # invoice on delivery + inv_id = self.so.action_invoice_create() + self.assertEqual(self.so.invoice_status, 'invoiced', + 'Sale Stock: so invoice_status should be "fully invoiced" after complete delivery and invoicing') + + def test_01_sale_stock_order(self): + """ + Test SO's changes when playing around with stock moves, quants, pack operations, pickings + and whatever other model there is in stock with "invoice on order" products + """ + # let's cheat and put all our products to "invoice on order" + self.so = self.env['sale.order'].create({ + 'partner_id': self.partner.id, + 'partner_invoice_id': self.partner.id, + 'partner_shipping_id': self.partner.id, + 'order_line': [(0, 0, {'name': p.name, 'product_id': p.id, 'product_uom_qty': 2, 'product_uom': p.uom_id.id, 'price_unit': p.list_price}) for (_, p) in self.products.iteritems()], + 'pricelist_id': self.env.ref('product.list0').id, + 'picking_policy': 'direct', + }) + for sol in self.so.order_line: + sol.product_id.invoice_policy = 'order' + # confirm our standard so, check the picking + self.so.action_confirm() + self.assertTrue(self.so.picking_ids, 'Sale Stock: no picking created for "invoice on order" stockable products') + # let's do an invoice for a deposit of 5% + adv_wiz = self.env['sale.advance.payment.inv'].with_context(active_ids=[self.so.id]).create({ + 'advance_payment_method': 'percentage', + 'amount': 5.0 + }) + act = adv_wiz.with_context(open_invoices=True).create_invoices() + inv = self.env['account.invoice'].browse(act['res_id']) + self.assertEqual(inv.amount_untaxed, self.so.amount_untaxed * 5.0 / 100.0, 'Sale Stock: deposit invoice is wrong') + self.assertEqual(self.so.invoice_status, 'to invoice', 'Sale Stock: so should be to invoice after invoicing deposit') + # invoice on order: everything should be invoiced + self.so.action_invoice_create() + self.assertEqual(self.so.invoice_status, 'invoiced', 'Sale Stock: so should be fully invoiced after second invoice') + + # deliver, check the delivered quantities + pick = self.so.picking_ids + pick.force_assign() + pick.pack_operation_product_ids.write({'qty_done': 2}) + self.assertIsNone(pick.do_new_transfer(), 'Sale Stock: complete delivery should not need a backorder') + del_qties = [sol.qty_delivered for sol in self.so.order_line] + del_qties_truth = [2.0 if sol.product_id.type in ['product', 'consu'] else 0.0 for sol in self.so.order_line] + self.assertEqual(del_qties, del_qties_truth, 'Sale Stock: delivered quantities are wrong after partial delivery') + # invoice on delivery: nothing to invoice + self.assertFalse(self.so.action_invoice_create(), 'Sale Stock: there should be nothing to invoice') + + def test_02_sale_stock_return(self): + """ + Test a SO with a product invoiced on delivery. Deliver and invoice the SO, then do a return + of the picking. Check that a refund invoice is well generated. + """ + # intial so + self.partner = self.env.ref('base.res_partner_1') + self.product = self.env.ref('product.product_product_47') + so_vals = { + 'partner_id': self.partner.id, + 'partner_invoice_id': self.partner.id, + 'partner_shipping_id': self.partner.id, + 'order_line': [(0, 0, { + 'name': self.product.name, + 'product_id': self.product.id, + 'product_uom_qty': 5.0, + 'product_uom': self.product.uom_id.id, + 'price_unit': self.product.list_price})], + 'pricelist_id': self.env.ref('product.list0').id, + } + self.so = self.env['sale.order'].create(so_vals) + + # confirm our standard so, check the picking + self.so.action_confirm() + self.assertTrue(self.so.picking_ids, 'Sale Stock: no picking created for "invoice on delivery" stockable products') + + # invoice in on delivery, nothing should be invoiced + self.assertEqual(self.so.invoice_status, 'no', 'Sale Stock: so invoice_status should be "nothing to invoice"') + + # deliver completely + pick = self.so.picking_ids + pick.force_assign() + pick.pack_operation_product_ids.write({'qty_done': 5}) + pick.do_new_transfer() + + # Check quantity delivered + del_qty = sum(sol.qty_delivered for sol in self.so.order_line) + self.assertEqual(del_qty, 5.0, 'Sale Stock: delivered quantity should be 5.0 after complete delivery') + + # Check invoice + self.assertEqual(self.so.invoice_status, 'to invoice', 'Sale Stock: so invoice_status should be "to invoice" before invoicing') + inv_1_id = self.so.action_invoice_create() + self.assertEqual(self.so.invoice_status, 'invoiced', 'Sale Stock: so invoice_status should be "invoiced" after invoicing') + self.assertEqual(len(inv_1_id), 1, 'Sale Stock: only one invoice should be created') + self.inv_1 = self.env['account.invoice'].browse(inv_1_id) + self.assertEqual(self.inv_1.amount_untaxed, self.inv_1.amount_untaxed, 'Sale Stock: amount in SO and invoice should be the same') + + # Create return picking + StockReturnPicking = self.env['stock.return.picking'] + default_data = StockReturnPicking.with_context(active_ids=pick.ids, active_id=pick.ids[0]).default_get(['move_dest_exists', 'original_location_id', 'product_return_moves', 'parent_location_id', 'location_id']) + return_wiz = StockReturnPicking.with_context(active_ids=pick.ids, active_id=pick.ids[0]).create(default_data) + res = return_wiz.create_returns() + return_pick = self.env['stock.picking'].browse(res['res_id']) + + # Validate picking + return_pick.force_assign() + return_pick.pack_operation_product_ids.write({'qty_done': 5}) + return_pick.do_new_transfer() + + # Check invoice + self.assertEqual(self.so.invoice_status, 'to invoice', 'Sale Stock: so invoice_status should be "to invoice" before invoicing') + # let's do an invoice with refunds + adv_wiz = self.env['sale.advance.payment.inv'].with_context(active_ids=[self.so.id]).create({ + 'advance_payment_method': 'all', + }) + adv_wiz.with_context(open_invoices=True).create_invoices() + self.inv_2 = self.so.invoice_ids[1] + self.assertEqual(self.so.invoice_status, 'no', 'Sale Stock: so invoice_status should be "no" after invoicing the return') + self.assertEqual(self.inv_2.amount_untaxed, self.inv_2.amount_untaxed, 'Sale Stock: amount in SO and invoice should be the same') -- GitLab