[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:
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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,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
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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,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
|
||||
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
import test_sale_stock
|
||||
@@ -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')
|
||||
Reference in New Issue
Block a user