--- /dev/null
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+
+from openerp.osv import fields
+from openerp.osv import osv
+import openerp.addons.decimal_precision as dp
+from openerp.tools.translate import _
+
+class mrp_subproduct(osv.osv):
+ _name = 'mrp.subproduct'
+ _description = 'Byproduct'
+ _columns={
+ 'product_id': fields.many2one('product.product', 'Product', required=True),
+ 'product_qty': fields.float('Product Qty', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
+ 'product_uom': fields.many2one('product.uom', 'Product Unit of Measure', required=True),
+ 'subproduct_type': fields.selection([('fixed','Fixed'),('variable','Variable')], 'Quantity Type', required=True, help="Define how the quantity of byproducts will be set on the production orders using this BoM.\
+ 'Fixed' depicts a situation where the quantity of created byproduct is always equal to the quantity set on the BoM, regardless of how many are created in the production order.\
+ By opposition, 'Variable' means that the quantity will be computed as\
+ '(quantity of byproduct set on the BoM / quantity of manufactured product set on the BoM * quantity of manufactured product in the production order.)'"),
+ 'bom_id': fields.many2one('mrp.bom', 'BoM'),
+ }
+ _defaults={
+ 'subproduct_type': 'variable',
+ 'product_qty': lambda *a: 1.0,
+ }
+
+ def onchange_product_id(self, cr, uid, ids, product_id, context=None):
+ """ Changes UoM if product_id changes.
+ @param product_id: Changed product_id
+ @return: Dictionary of changed values
+ """
+ if product_id:
+ prod = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
+ v = {'product_uom': prod.uom_id.id}
+ return {'value': v}
+ return {}
+
+ def onchange_uom(self, cr, uid, ids, product_id, product_uom, context=None):
+ res = {'value':{}}
+ if not product_uom or not product_id:
+ return res
+ product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
+ uom = self.pool.get('product.uom').browse(cr, uid, product_uom, context=context)
+ if uom.category_id.id != product.uom_id.category_id.id:
+ res['warning'] = {'title': _('Warning'), 'message': _('The Product Unit of Measure you chose has a different category than in the product form.')}
+ res['value'].update({'product_uom': product.uom_id.id})
+ return res
+
+mrp_subproduct()
+
+class mrp_bom(osv.osv):
+ _name = 'mrp.bom'
+ _description = 'Bill of Material'
+ _inherit='mrp.bom'
+
+ _columns={
+ 'sub_products':fields.one2many('mrp.subproduct', 'bom_id', 'Byproducts'),
+ }
+
+mrp_bom()
+
+class mrp_production(osv.osv):
+ _description = 'Production'
+ _inherit= 'mrp.production'
+
+
+ def action_confirm(self, cr, uid, ids):
+ """ Confirms production order and calculates quantity based on subproduct_type.
+ @return: Newly generated picking Id.
+ """
+ picking_id = super(mrp_production,self).action_confirm(cr, uid, ids)
+ product_uom_obj = self.pool.get('product.uom')
+ for production in self.browse(cr, uid, ids):
- source = production.product_id.product_tmpl_id.property_stock_production.id
++ source = production.product_id.property_stock_production.id
+ if not production.bom_id:
+ continue
+ for sub_product in production.bom_id.sub_products:
+ product_uom_factor = product_uom_obj._compute_qty(cr, uid, production.product_uom.id, production.product_qty, production.bom_id.product_uom.id)
+ qty1 = sub_product.product_qty
+ qty2 = production.product_uos and production.product_uos_qty or False
+ product_uos_factor = 0.0
+ if qty2 and production.bom_id.product_uos.id:
+ product_uos_factor = product_uom_obj._compute_qty(cr, uid, production.product_uos.id, production.product_uos_qty, production.bom_id.product_uos.id)
+ if sub_product.subproduct_type == 'variable':
+ if production.product_qty:
+ qty1 *= product_uom_factor / (production.bom_id.product_qty or 1.0)
+ if production.product_uos_qty:
+ qty2 *= product_uos_factor / (production.bom_id.product_uos_qty or 1.0)
+ data = {
+ 'name': 'PROD:'+production.name,
+ 'date': production.date_planned,
+ 'product_id': sub_product.product_id.id,
+ 'product_qty': qty1,
+ 'product_uom': sub_product.product_uom.id,
+ 'product_uos_qty': qty2,
+ 'product_uos': production.product_uos and production.product_uos.id or False,
+ 'location_id': source,
+ 'location_dest_id': production.location_dest_id.id,
+ 'move_dest_id': production.move_prod_id.id,
+ 'state': 'waiting',
+ 'production_id': production.id
+ }
+ self.pool.get('stock.move').create(cr, uid, data)
+ return picking_id
+
+ def _get_subproduct_factor(self, cr, uid, production_id, move_id=None, context=None):
+ """Compute the factor to compute the qty of procucts to produce for the given production_id. By default,
+ it's always equal to the quantity encoded in the production order or the production wizard, but with
+ the module mrp_byproduct installed it can differ for byproducts having type 'variable'.
+ :param production_id: ID of the mrp.order
+ :param move_id: ID of the stock move that needs to be produced. Identify the product to produce.
+ :return: The factor to apply to the quantity that we should produce for the given production order and stock move.
+ """
+ sub_obj = self.pool.get('mrp.subproduct')
+ move_obj = self.pool.get('stock.move')
+ production_obj = self.pool.get('mrp.production')
+ production_browse = production_obj.browse(cr, uid, production_id, context=context)
+ move_browse = move_obj.browse(cr, uid, move_id, context=context)
+ subproduct_factor = 1
+ sub_id = sub_obj.search(cr, uid,[('product_id', '=', move_browse.product_id.id),('bom_id', '=', production_browse.bom_id.id), ('subproduct_type', '=', 'variable')], context=context)
+ if sub_id:
+ subproduct_record = sub_obj.browse(cr ,uid, sub_id[0], context=context)
+ if subproduct_record.bom_id.product_qty:
+ subproduct_factor = subproduct_record.product_qty / subproduct_record.bom_id.product_qty
+ return subproduct_factor
+ return super(mrp_production, self)._get_subproduct_factor(cr, uid, production_id, move_id, context=context)
+
+mrp_production()
+
+class change_production_qty(osv.osv_memory):
+ _inherit = 'change.production.qty'
+
+ def _update_product_to_produce(self, cr, uid, prod, qty, context=None):
+ bom_obj = self.pool.get('mrp.bom')
+ move_lines_obj = self.pool.get('stock.move')
+ prod_obj = self.pool.get('mrp.production')
+ for m in prod.move_created_ids:
+ if m.product_id.id == prod.product_id.id:
+ move_lines_obj.write(cr, uid, [m.id], {'product_qty': qty})
+ else:
+ for sub_product_line in prod.bom_id.sub_products:
+ if sub_product_line.product_id.id == m.product_id.id:
+ factor = prod_obj._get_subproduct_factor(cr, uid, prod.id, m.id, context=context)
+ subproduct_qty = sub_product_line.subproduct_type == 'variable' and qty * factor or sub_product_line.product_qty
+ move_lines_obj.write(cr, uid, [m.id], {'product_qty': subproduct_qty})
+
+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
--- /dev/null
+# -*- coding: utf-8 -*-
+##############################################################################
+#
+# OpenERP, Open Source Management Solution
+# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+##############################################################################
+from datetime import datetime, timedelta
+from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP, float_compare
+from dateutil.relativedelta import relativedelta
+from openerp.osv import fields, osv
+from openerp import netsvc
+from openerp.tools.translate import _
+
+class sale_shop(osv.osv):
+ _inherit = "sale.shop"
+ _columns = {
+ 'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse'),
+ }
+
+sale_shop()
+
+class sale_order(osv.osv):
+ _inherit = "sale.order"
+
+ def copy(self, cr, uid, id, default=None, context=None):
+ if not default:
+ default = {}
+ default.update({
+ 'shipped': False,
+ 'picking_ids': [],
+ })
+ return super(sale_order, self).copy(cr, uid, id, default, context=context)
+
+ def shipping_policy_change(self, cr, uid, ids, policy, context=None):
+ if not policy:
+ return {}
+ inv_qty = 'order'
+ if policy == 'prepaid':
+ inv_qty = 'order'
+ elif policy == 'picking':
+ inv_qty = 'procurement'
+ return {'value': {'invoice_quantity': inv_qty}}
+
+ def write(self, cr, uid, ids, vals, context=None):
+ if vals.get('order_policy', False):
+ if vals['order_policy'] == 'prepaid':
+ vals.update({'invoice_quantity': 'order'})
+ elif vals['order_policy'] == 'picking':
+ vals.update({'invoice_quantity': 'procurement'})
+ return super(sale_order, self).write(cr, uid, ids, vals, context=context)
+
+ def create(self, cr, uid, vals, context=None):
+ if vals.get('order_policy', False):
+ if vals['order_policy'] == 'prepaid':
+ vals.update({'invoice_quantity': 'order'})
+ if vals['order_policy'] == 'picking':
+ vals.update({'invoice_quantity': 'procurement'})
+ order = super(sale_order, self).create(cr, uid, vals, context=context)
+ return order
+
+ # This is False
+ def _picked_rate(self, cr, uid, ids, name, arg, context=None):
+ if not ids:
+ return {}
+ res = {}
+ tmp = {}
+ for id in ids:
+ tmp[id] = {'picked': 0.0, 'total': 0.0}
+ cr.execute('''SELECT
+ p.sale_id as sale_order_id, sum(m.product_qty) as nbr, mp.state as procurement_state, m.state as move_state, p.type as picking_type
+ FROM
+ stock_move m
+ LEFT JOIN
+ stock_picking p on (p.id=m.picking_id)
+ LEFT JOIN
+ procurement_order mp on (mp.move_id=m.id)
+ WHERE
+ p.sale_id IN %s GROUP BY m.state, mp.state, p.sale_id, p.type''', (tuple(ids),))
+
+ for item in cr.dictfetchall():
+ if item['move_state'] == 'cancel':
+ continue
+
+ if item['picking_type'] == 'in':#this is a returned picking
+ tmp[item['sale_order_id']]['total'] -= item['nbr'] or 0.0 # Deducting the return picking qty
+ if item['procurement_state'] == 'done' or item['move_state'] == 'done':
+ tmp[item['sale_order_id']]['picked'] -= item['nbr'] or 0.0
+ else:
+ tmp[item['sale_order_id']]['total'] += item['nbr'] or 0.0
+ if item['procurement_state'] == 'done' or item['move_state'] == 'done':
+ tmp[item['sale_order_id']]['picked'] += item['nbr'] or 0.0
+
+ for order in self.browse(cr, uid, ids, context=context):
+ if order.shipped:
+ res[order.id] = 100.0
+ else:
+ res[order.id] = tmp[order.id]['total'] and (100.0 * tmp[order.id]['picked'] / tmp[order.id]['total']) or 0.0
+ return res
+
+ _columns = {
+ 'state': fields.selection([
+ ('draft', 'Draft Quotation'),
+ ('sent', 'Quotation Sent'),
+ ('cancel', 'Cancelled'),
+ ('waiting_date', 'Waiting Schedule'),
+ ('progress', 'Sale Order'),
+ ('manual', 'Sale to Invoice'),
+ ('shipping_except', 'Shipping Exception'),
+ ('invoice_except', 'Invoice Exception'),
+ ('done', 'Done'),
+ ], 'Status', readonly=True,help="Gives the status of the quotation or sales order.\
+ \nThe exception status is automatically set when a cancel operation occurs \
+ in the invoice validation (Invoice Exception) or in the picking list process (Shipping Exception).\nThe 'Waiting Schedule' status is set when the invoice is confirmed\
+ but waiting for the scheduler to run on the order date.", select=True),
+ 'incoterm': fields.many2one('stock.incoterms', 'Incoterm', 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."""),
+ 'picking_ids': fields.one2many('stock.picking.out', 'sale_id', 'Related Picking', readonly=True, help="This is a list of delivery orders that has been generated for this sales order."),
+ 'shipped': fields.boolean('Delivered', readonly=True, help="It indicates that the sales order has been delivered. This field is updated only after the scheduler(s) have been launched."),
+ 'picked_rate': fields.function(_picked_rate, string='Picked', type='float'),
+ 'invoice_quantity': fields.selection([('order', 'Ordered Quantities'), ('procurement', 'Shipped Quantities')], 'Invoice on',
+ help="The sale order will automatically create the invoice proposition (draft invoice).\
+ You have to choose if you want your invoice based on ordered ", required=True, readonly=True, states={'draft': [('readonly', False)]}),
+ }
+ _defaults = {
+ 'picking_policy': 'direct',
+ 'order_policy': 'manual',
+ 'invoice_quantity': 'order',
+ }
+
+ # Form filling
+ def unlink(self, cr, uid, ids, context=None):
+ sale_orders = self.read(cr, uid, ids, ['state'], context=context)
+ unlink_ids = []
+ for s in sale_orders:
+ if s['state'] in ['draft', 'cancel']:
+ unlink_ids.append(s['id'])
+ else:
+ raise osv.except_osv(_('Invalid Action!'), _('In order to delete a confirmed sales order, you must cancel it.\nTo do so, you must first cancel related picking for delivery orders.'))
+
+ return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
+
+ def action_view_delivery(self, cr, uid, ids, context=None):
+ '''
+ This function returns an action that display existing delivery orders of given sale 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')
+ id = result and result[1] or False
+ result = act_obj.read(cr, uid, [id], context=context)[0]
+ #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]
+ #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_out_form')
+ result['views'] = [(res and res[1] or False, 'form')]
+ result['res_id'] = pick_ids and pick_ids[0] or False
+ return result
+
+ def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_invoice = False, context=None):
+ picking_obj = self.pool.get('stock.picking')
+ 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':
+ picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
+ return res
+
+ def action_cancel(self, cr, uid, ids, context=None):
+ wf_service = netsvc.LocalService("workflow")
+ if context is None:
+ context = {}
+ sale_order_line_obj = self.pool.get('sale.order.line')
+ proc_obj = self.pool.get('procurement.order')
+ for sale in self.browse(cr, uid, ids, context=context):
+ for pick in sale.picking_ids:
+ if pick.state not in ('draft', 'cancel'):
+ raise osv.except_osv(
+ _('Cannot cancel sales order!'),
+ _('You must first cancel all delivery order(s) attached to this sales order.'))
+ if pick.state == 'cancel':
+ for mov in pick.move_lines:
+ proc_ids = proc_obj.search(cr, uid, [('move_id', '=', mov.id)])
+ if proc_ids:
+ for proc in proc_ids:
+ wf_service.trg_validate(uid, 'procurement.order', proc, 'button_check', cr)
+ for r in self.read(cr, uid, ids, ['picking_ids']):
+ for pick in r['picking_ids']:
+ wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
+ return super(sale_order, self).action_cancel(cr, uid, ids, context=context)
+
+ 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)
+ return res
+
+ def procurement_lines_get(self, cr, uid, ids, *args):
+ res = []
+ for order in self.browse(cr, uid, ids, context={}):
+ for line in order.order_line:
+ if line.procurement_id:
+ res.append(line.procurement_id.id)
+ return res
+
+ # if mode == 'finished':
+ # returns True if all lines are done, False otherwise
+ # if mode == 'canceled':
+ # returns True if there is at least one canceled line, False otherwise
+ def test_state(self, cr, uid, ids, mode, *args):
+ assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
+ finished = True
+ canceled = False
+ write_done_ids = []
+ write_cancel_ids = []
+ for order in self.browse(cr, uid, ids, context={}):
+ for line in order.order_line:
+ if (not line.procurement_id) or (line.procurement_id.state=='done'):
+ if line.state != 'done':
+ write_done_ids.append(line.id)
+ else:
+ finished = False
+ if line.procurement_id:
+ if (line.procurement_id.state == 'cancel'):
+ canceled = True
+ if line.state != 'exception':
+ write_cancel_ids.append(line.id)
+ if write_done_ids:
+ self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
+ if write_cancel_ids:
+ self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
+
+ if mode == 'finished':
+ return finished
+ elif mode == 'canceled':
+ return canceled
+
+ def _prepare_order_line_procurement(self, cr, uid, order, line, move_id, date_planned, context=None):
+ return {
+ 'name': line.name,
+ 'origin': order.name,
+ 'date_planned': date_planned,
+ 'product_id': line.product_id.id,
+ 'product_qty': line.product_uom_qty,
+ 'product_uom': line.product_uom.id,
+ 'product_uos_qty': (line.product_uos and line.product_uos_qty)\
+ or line.product_uom_qty,
+ 'product_uos': (line.product_uos and line.product_uos.id)\
+ or line.product_uom.id,
+ 'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
+ 'procure_method': line.type,
+ 'move_id': move_id,
+ 'company_id': order.company_id.id,
+ 'note': line.name,
+ }
+
+ def _prepare_order_line_move(self, cr, uid, order, line, picking_id, date_planned, context=None):
+ location_id = order.shop_id.warehouse_id.lot_stock_id.id
+ output_id = order.shop_id.warehouse_id.lot_output_id.id
+ return {
+ 'name': line.name,
+ 'picking_id': picking_id,
+ 'product_id': line.product_id.id,
+ 'date': date_planned,
+ 'date_expected': date_planned,
+ 'product_qty': line.product_uom_qty,
+ 'product_uom': line.product_uom.id,
+ 'product_uos_qty': (line.product_uos and line.product_uos_qty) or line.product_uom_qty,
+ 'product_uos': (line.product_uos and line.product_uos.id)\
+ or line.product_uom.id,
+ 'product_packaging': line.product_packaging.id,
+ 'partner_id': line.address_allotment_id.id or order.partner_shipping_id.id,
+ 'location_id': location_id,
+ 'location_dest_id': output_id,
+ 'sale_line_id': line.id,
+ 'tracking_id': False,
+ 'state': 'draft',
+ #'state': 'waiting',
+ 'company_id': order.company_id.id,
+ 'price_unit': line.product_id.standard_price or 0.0
+ }
+
+ def _prepare_order_picking(self, cr, uid, order, context=None):
+ pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
+ return {
+ 'name': pick_name,
+ 'origin': order.name,
+ 'date': order.date_order,
+ 'type': 'out',
+ 'state': 'auto',
+ 'move_type': order.picking_policy,
+ 'sale_id': order.id,
+ 'partner_id': order.partner_shipping_id.id,
+ 'note': order.note,
+ 'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
+ 'company_id': order.company_id.id,
+ }
+
+ def ship_recreate(self, cr, uid, order, line, move_id, proc_id):
+ # FIXME: deals with potentially cancelled shipments, seems broken (specially if shipment has production lot)
+ """
+ Define ship_recreate for process after shipping exception
+ param order: sale order to which the order lines belong
+ param line: sale order line records to procure
+ param move_id: the ID of stock move
+ param proc_id: the ID of procurement
+ """
+ move_obj = self.pool.get('stock.move')
+ if order.state == 'shipping_except':
+ for pick in order.picking_ids:
+ for move in pick.move_lines:
+ if move.state == 'cancel':
+ mov_ids = move_obj.search(cr, uid, [('state', '=', 'cancel'),('sale_line_id', '=', line.id),('picking_id', '=', pick.id)])
+ if mov_ids:
+ for mov in move_obj.browse(cr, uid, mov_ids):
+ # FIXME: the following seems broken: what if move_id doesn't exist? What if there are several mov_ids? Shouldn't that be a sum?
+ move_obj.write(cr, uid, [move_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
+ self.pool.get('procurement.order').write(cr, uid, [proc_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
+ return True
+
+ def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
+ date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATE_FORMAT) + relativedelta(days=line.delay or 0.0)
+ date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
+ return date_planned
+
+ def _create_pickings_and_procurements(self, cr, uid, order, order_lines, picking_id=False, context=None):
+ """Create the required procurements to supply sale order lines, also connecting
+ the procurements to appropriate stock moves in order to bring the goods to the
+ sale order's requested location.
+
+ If ``picking_id`` is provided, the stock moves will be added to it, otherwise
+ a standard outgoing picking will be created to wrap the stock moves, as returned
+ by :meth:`~._prepare_order_picking`.
+
+ Modules that wish to customize the procurements or partition the stock moves over
+ multiple stock pickings may override this method and call ``super()`` with
+ different subsets of ``order_lines`` and/or preset ``picking_id`` values.
+
+ :param browse_record order: sale order to which the order lines belong
+ :param list(browse_record) order_lines: sale order line records to procure
+ :param int picking_id: optional ID of a stock picking to which the created stock moves
+ will be added. A new picking will be created if ommitted.
+ :return: True
+ """
+ move_obj = self.pool.get('stock.move')
+ picking_obj = self.pool.get('stock.picking')
+ procurement_obj = self.pool.get('procurement.order')
+ proc_ids = []
+
+ for line in order_lines:
+ if line.state == 'done':
+ continue
+
+ date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context)
+
+ if line.product_id:
- if line.product_id.product_tmpl_id.type in ('product', 'consu'):
++ if line.product_id.type in ('product', 'consu'):
+ if not picking_id:
+ picking_id = picking_obj.create(cr, uid, self._prepare_order_picking(cr, uid, order, context=context))
+ move_id = move_obj.create(cr, uid, self._prepare_order_line_move(cr, uid, order, line, picking_id, date_planned, context=context))
+ else:
+ # a service has no stock move
+ move_id = False
+
+ proc_id = procurement_obj.create(cr, uid, self._prepare_order_line_procurement(cr, uid, order, line, move_id, date_planned, context=context))
+ proc_ids.append(proc_id)
+ line.write({'procurement_id': proc_id})
+ self.ship_recreate(cr, uid, order, line, move_id, proc_id)
+
+ wf_service = netsvc.LocalService("workflow")
+ if picking_id:
+ wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
+ self.delivery_send_note(cr, uid, [order.id], picking_id, context)
+
+
+ for proc_id in proc_ids:
+ wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
+
+ val = {}
+ if order.state == 'shipping_except':
+ val['state'] = 'progress'
+ val['shipped'] = False
+
+ 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
+ order.write(val)
+ return True
+
+ def action_ship_create(self, cr, uid, ids, context=None):
+ for order in self.browse(cr, uid, ids, context=context):
+ self._create_pickings_and_procurements(cr, uid, order, order.order_line, None, context=context)
+ return True
+
+ def action_ship_end(self, cr, uid, ids, context=None):
+ 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
+ for line in order.order_line:
+ towrite = []
+ if line.state == 'exception':
+ towrite.append(line.id)
+ if towrite:
+ self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
+ res = self.write(cr, uid, [order.id], val)
+ if res:
+ self.delivery_end_send_note(cr, uid, [order.id], context=context)
+ 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.product_id and order_line.product_id.product_tmpl_id.type in ('product', 'consu'):
++ if order_line.product_id and order_line.product_id.type in ('product', 'consu'):
+ return True
+ return False
+
+ # ------------------------------------------------
+ # OpenChatter methods and notifications
+ # ------------------------------------------------
+
+ def get_needaction_user_ids(self, cr, uid, ids, context=None):
+ result = super(sale_order, self).get_needaction_user_ids(cr, uid, ids, context=context)
+ return result
+
+ def delivery_send_note(self, cr, uid, ids, picking_id, context=None):
+ for order in self.browse(cr, uid, ids, context=context):
+ for picking in (pck for pck in order.picking_ids if pck.id == picking_id):
+ # convert datetime field to a datetime, using server format, then
+ # convert it to the user TZ and re-render it with %Z to add the timezone
+ picking_datetime = fields.DT.datetime.strptime(picking.min_date, DEFAULT_SERVER_DATETIME_FORMAT)
+ picking_date_str = fields.datetime.context_timestamp(cr, uid, picking_datetime, context=context).strftime(DATETIME_FORMATS_MAP['%+'] + " (%Z)")
+ self.message_post(cr, uid, [order.id], body=_("Delivery Order <em>%s</em> <b>scheduled</b> for %s.") % (picking.name, picking_date_str), context=context)
+
+ def delivery_end_send_note(self, cr, uid, ids, context=None):
+ self.message_post(cr, uid, ids, body=_("Order <b>delivered</b>."), context=context)
+
+class sale_order_line(osv.osv):
+
+ 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
+
+ _inherit = 'sale.order.line'
+ _columns = {
+ 'delay': fields.float('Delivery Lead Time', required=True, help="Number of days between the order confirmation and the shipping of the products to the customer", readonly=True, states={'draft': [('readonly', False)]}),
+ 'procurement_id': fields.many2one('procurement.order', 'Procurement'),
+ 'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft': [('readonly', False)]}),
+ 'product_packaging': fields.many2one('product.packaging', 'Packaging'),
+ 'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
+ 'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'),
+ }
+ _defaults = {
+ 'delay': 0.0,
+ 'product_packaging': False,
+ }
+
+ def _get_line_qty(self, cr, uid, line, context=None):
+ if line.procurement_id and not (line.order_id.invoice_quantity=='order'):
+ return self.pool.get('procurement.order').quantity_get(cr, uid,
+ line.procurement_id.id, context=context)
+ else:
+ return super(sale_order_line, self)._get_line_qty(cr, uid, line, context=context)
+
+
+ def _get_line_uom(self, cr, uid, line, context=None):
+ if line.procurement_id and not (line.order_id.invoice_quantity=='order'):
+ return self.pool.get('procurement.order').uom_get(cr, uid,
+ line.procurement_id.id, context=context)
+ else:
+ return super(sale_order_line, self)._get_line_uom(cr, uid, line, context=context)
+
+ def button_cancel(self, cr, uid, ids, context=None):
+ res = super(sale_order_line, self).button_cancel(cr, uid, ids, context=context)
+ for line in self.browse(cr, uid, ids, context=context):
+ for move_line in line.move_ids:
+ if move_line.state != 'cancel':
+ raise osv.except_osv(
+ _('Cannot cancel sales order line!'),
+ _('You must first cancel stock moves attached to this sales order line.'))
+ return res
+
+ def copy_data(self, cr, uid, id, default=None, context=None):
+ if not default:
+ default = {}
+ default.update({'move_ids': []})
+ return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
+
+ 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']['message']
+
+ products = product_obj.browse(cr, uid, product, context=context)
+ if not products.packaging:
+ packaging = result['product_packaging'] = False
+ elif not packaging and products.packaging and not flag:
+ packaging = products.packaging[0].id
+ result['product_packaging'] = packaging
+
+ 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):
+ ean = pack.ean 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"
+ "EAN: %s Quantity: %s Type of ul: %s") % \
+ (qty, ean, 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 product_id_change(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=False, flag=False, context=None):
+ context = context or {}
+ product_uom_obj = self.pool.get('product.uom')
+ partner_obj = self.pool.get('res.partner')
+ product_obj = self.pool.get('product.product')
+ warning = {}
+ res = super(sale_order_line, self).product_id_change(cr, uid, ids, pricelist, product, qty=qty,
+ uom=uom, qty_uos=qty_uos, uos=uos, name=name, partner_id=partner_id,
+ lang=lang, update_tax=update_tax, date_order=date_order, packaging=packaging, fiscal_position=fiscal_position, flag=flag, context=context)
+
+ if not product:
+ res['value'].update({'product_packaging': False})
+ return res
+
+ #update of result obtained in super function
+ 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 ''
+ product_obj = product_obj.browse(cr, uid, product, context=context)
+ res['value']['delay'] = (product_obj.sale_delay or 0.0)
+ res['value']['type'] = product_obj.procure_method
+
+ #check if product is available, and if not: raise an error
+ uom2 = False
+ if uom:
+ uom2 = product_uom_obj.browse(cr, uid, uom)
+ if product_obj.uom_id.category_id.id != uom2.category_id.id:
+ uom = False
+ if not uom2:
+ uom2 = product_obj.uom_id
+ compare_qty = float_compare(product_obj.virtual_available * uom2.factor, qty * product_obj.uom_id.factor, precision_rounding=product_obj.uom_id.rounding)
+ if (product_obj.type=='product') and int(compare_qty) == -1 \
+ and (product_obj.procure_method=='make_to_stock'):
+ warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \
+ (qty, uom2 and uom2.name or product_obj.uom_id.name,
+ max(0,product_obj.virtual_available), product_obj.uom_id.name,
+ max(0,product_obj.qty_available), product_obj.uom_id.name)
+ warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n"
+
+ #update of warning messages
+ if warning_msgs:
+ warning = {
+ 'title': _('Configuration Error!'),
+ 'message' : warning_msgs
+ }
+ res.update({'warning': warning})
+ return res
+
+
+class sale_advance_payment_inv(osv.osv_memory):
+ _inherit = "sale.advance.payment.inv"
+
+ def _create_invoices(self, cr, uid, inv_values, sale_id, context=None):
+ result = super(sale_advance_payment_inv, self)._create_invoices(cr, uid, inv_values, sale_id, context=context)
+ sale_obj = self.pool.get('sale.order')
+ sale_line_obj = self.pool.get('sale.order.line')
+ wizard = self.browse(cr, uid, [result], context)
+ sale = sale_obj.browse(cr, uid, sale_id, context=context)
+ if sale.order_policy == 'postpaid':
+ raise osv.except_osv(
+ _('Error!'),
+ _("You cannot make an advance on a sales order \
+ that is defined as 'Automatic Invoice after delivery'."))
+
+ # If invoice on picking: add the cost on the SO
+ # If not, the advance will be deduced when generating the final invoice
+ line_name = inv_values.get('invoice_line') and inv_values.get('invoice_line')[0][2].get('name') or ''
+ line_tax = inv_values.get('invoice_line') and inv_values.get('invoice_line')[0][2].get('invoice_line_tax_id') or False
+ if sale.order_policy == 'picking':
+ vals = {
+ 'order_id': sale.id,
+ 'name': line_name,
+ 'price_unit': -inv_amount,
+ 'product_uom_qty': wizard.qtty or 1.0,
+ 'product_uos_qty': wizard.qtty or 1.0,
+ 'product_uos': res.get('uos_id', False),
+ 'product_uom': res.get('uom_id', False),
+ 'product_id': wizard.product_id.id or False,
+ 'discount': False,
+ 'tax_id': line_tax,
+ }
+ sale_line_obj.create(cr, uid, vals, context=context)
+ return result
--- /dev/null
+-
+ In order to test process of the Sale Order,
+-
+ First I check the total amount of the Quotation before Approved.
+-
+ !assert {model: sale.order, id: sale.sale_order_6, string: The amount of the Quotation is not correctly computed}:
+ - sum([l.price_subtotal for l in order_line]) == amount_untaxed
+-
+ I confirm the quotation with Invoice based on deliveries policy.
+-
+ !workflow {model: sale.order, action: order_confirm, ref: sale.sale_order_6}
+-
+ 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.sale_order_6"))
+ 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 tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
+ order = self.browse(cr, uid, ref("sale.sale_order_6"))
+ for order_line in order.order_line:
+ procurement = order_line.procurement_id
+ date_planned = datetime.strptime(order.date_order, DEFAULT_SERVER_DATE_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."
+ assert procurement.procure_method == order_line.type, "Procurement method is not correspond."
+-
+ I run the scheduler.
+-
+ !python {model: procurement.order}: |
+ self.run_scheduler(cr, uid)
+-
+ 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 tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
+ sale_order = self.browse(cr, uid, ref("sale.sale_order_6"))
+ 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.type == 'out',"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), "Total move of delivery order are not corresposning with total sale order lines."
+ location_id = sale_order.shop_id.warehouse_id.lot_stock_id.id
+ output_id = sale_order.shop_id.warehouse_id.lot_output_id.id
+ for move in picking.move_lines:
+ order_line = move.sale_line_id
+ date_planned = datetime.strptime(sale_order.date_order, DEFAULT_SERVER_DATE_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 == (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.partner_id.id == order_line.address_allotment_id.id or sale_order.partner_shipping_id.id,"Address is not correspond"
+ #assert move.location_id.id == location_id,"Source Location is not correspond."
+ #assert move.location_dest_id == output_id,"Destination Location is not correspond."
+ assert move.price_unit == order_line.product_id.standard_price or 0.0,"Price Unit is not correspond"
+-
+ Now, I dispatch delivery order.
+-
+ !python {model: stock.partial.picking}: |
+ order = self.pool.get('sale.order').browse(cr, uid, ref("sale.sale_order_6"))
+ for pick in order.picking_ids:
+ data = pick.force_assign()
+ if data == True:
+ partial_id = self.create(cr, uid, {}, context={'active_model': 'stock.picking','active_ids': [pick.id]})
+ self.do_partial(cr, uid, [partial_id])
+-
+ I check sale order to verify shipment.
+-
+ !python {model: sale.order}: |
+ order = self.pool.get('sale.order').browse(cr, uid, ref("sale.sale_order_6"))
+ assert order.shipped == True, "Sale order is not Delivered."
+ assert order.picked_rate == 100, "Shipment progress is not 100%."
+ #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.sale_order_6"))
+ ship_ids = [x.id for x in sale_order.picking_ids]
+ wiz_id = self.create(cr, uid, {'journal_id': ref('account.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}: |
+ order = self.browse(cr, uid, ref("sale.sale_order_6"))
+ assert order.invoice_ids, "Invoice is not created."
+ ac = order.partner_id.property_account_receivable.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_id.id,"Customer is not correspond."
+ assert invoice.currency_id.id == order.pricelist_id.currency_id.id, "Currency is not correspond."
+ assert invoice.comment == (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 == order.payment_term.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.product_tmpl_id.property_account_income.id or so_line.product_id.categ_id.property_account_income_categ.id
++ ac = so_line.product_id.property_account_income.id or so_line.product_id.categ_id.property_account_income_categ.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 inv_line.price_unit == so_line.price_unit , "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."
+-
+ I open the Invoice.
+-
+ !python {model: sale.order}: |
+ import netsvc
+ wf_service = netsvc.LocalService("workflow")
+ so = self.browse(cr, uid, ref("sale.sale_order_6"))
+ for invoice in so.invoice_ids:
+ wf_service.trg_validate(uid, 'account.invoice', invoice.id, 'invoice_open', cr)
+-
+ I pay the invoice
+-
+ !python {model: account.invoice}: |
+ sale_order = self.pool.get('sale.order')
+ order = sale_order.browse(cr, uid, ref("sale.sale_order_6"))
+ journal_ids = self.pool.get('account.journal').search(cr, uid, [('type', '=', 'cash'), ('company_id', '=', order.company_id.id)], limit=1)
+ for invoice in order.invoice_ids:
+ invoice.pay_and_reconcile(
+ invoice.amount_total, ref('account.cash'), ref('account.period_8'),
+ journal_ids[0], ref('account.cash'),
+ ref('account.period_8'), journal_ids[0],
+ name='test')
+-
+ I check the order after paid invoice.
+-
+ !python {model: sale.order}: |
+ order = self.browse(cr, uid, ref("sale.sale_order_6"))
+ assert order.invoiced == True, "Sale order is not invoiced."
+ assert order.invoiced_rate == 100, "Invoiced progress is not 100%."
+ assert order.state == 'done', 'Order should be in closed.'
+-
+ I print a sale order report.
+-
+ !python {model: sale.order}: |
+ import netsvc, tools, os
+ (data, format) = netsvc.LocalService('report.sale.order').create(cr, uid, [ref('sale.sale_order_6')], {}, {})
+ if tools.config['test_report_directory']:
+ file(os.path.join(tools.config['test_report_directory'], 'sale-sale_order.'+format), 'wb+').write(data)
+