[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
This commit is contained in:
Nicolas Martinelli
2015-08-27 19:21:37 +02:00
parent de423ba138
commit e57ac16476
22 changed files with 447 additions and 1226 deletions
-11
View File
@@ -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,
}
+2 -2
View File
@@ -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>
+8 -13
View File
@@ -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.")
+1 -1
View File
@@ -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>
+7 -22
View File
@@ -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"
@@ -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>
+34 -46
View File
@@ -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'),
}
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')
@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
+1 -2
View File
@@ -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>
+200 -486
View File
@@ -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):
from datetime import datetime, timedelta
from openerp import api, fields, models, _
from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT, float_compare
class SaleOrder(models.Model):
_inherit = "sale.order"
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]
@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
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])
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:
res[sale.id] = False
return res
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 _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)
@api.onchange('warehouse_id')
def _onchange_warehouse_id(self):
if self.warehouse_id.company_id:
self.company_id = self.warehouse_id.company_id.id
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
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
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
_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')
action = self.env.ref('stock.action_picking_tree_all')
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]
#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_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):
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
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 _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):
_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
}
res.update({'warning': warning})
return res
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
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")
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,
}
pick_ids = sum([order.picking_ids.ids for order in self], [])
class stock_picking(osv.osv):
_inherit = "stock.picking"
if len(pick_ids) > 1:
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 _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"),
}
@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 _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]
@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
class account_invoice(osv.Model):
class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'
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)
}
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
@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 {}
class StockLocationRoute(models.Model):
_inherit = "stock.location.route"
sale_selectable = fields.Boolean(string="Selectable on Sales Order Line")
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
+2 -11
View File
@@ -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>
-1
View File
@@ -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')
+12 -125
View File
@@ -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 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']/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="//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="//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>
<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>
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
</data>
</openerp>
+3 -3
View File
@@ -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>
@@ -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'
@@ -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.'
@@ -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."
@@ -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)
@@ -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"
@@ -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
+3
View File
@@ -0,0 +1,3 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import test_sale_stock
+174
View File
@@ -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')