661 lines
33 KiB
Python
661 lines
33 KiB
Python
# -*- coding: utf-8 -*-
|
|
##############################################################################
|
|
#
|
|
# OpenERP, Open Source Management Solution
|
|
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as
|
|
# published by the Free Software Foundation, either version 3 of the
|
|
# License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
##############################################################################
|
|
from datetime import datetime, timedelta
|
|
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT, DATETIME_FORMATS_MAP, float_compare
|
|
from dateutil.relativedelta import relativedelta
|
|
from openerp.osv import fields, osv
|
|
from openerp import netsvc
|
|
from openerp.tools.translate import _
|
|
import pytz
|
|
from openerp import SUPERUSER_ID
|
|
|
|
class sale_shop(osv.osv):
|
|
_inherit = "sale.shop"
|
|
_columns = {
|
|
'warehouse_id': fields.many2one('stock.warehouse', 'Warehouse'),
|
|
}
|
|
|
|
sale_shop()
|
|
|
|
class sale_order(osv.osv):
|
|
_inherit = "sale.order"
|
|
|
|
def copy(self, cr, uid, id, default=None, context=None):
|
|
if not default:
|
|
default = {}
|
|
default.update({
|
|
'shipped': False,
|
|
'picking_ids': [],
|
|
})
|
|
return super(sale_order, self).copy(cr, uid, id, default, context=context)
|
|
|
|
def shipping_policy_change(self, cr, uid, ids, policy, context=None):
|
|
if not policy:
|
|
return {}
|
|
inv_qty = 'order'
|
|
if policy == 'prepaid':
|
|
inv_qty = 'order'
|
|
elif policy == 'picking':
|
|
inv_qty = 'procurement'
|
|
return {'value': {'invoice_quantity': inv_qty}}
|
|
|
|
def write(self, cr, uid, ids, vals, context=None):
|
|
if vals.get('order_policy', False):
|
|
if vals['order_policy'] == 'prepaid':
|
|
vals.update({'invoice_quantity': 'order'})
|
|
elif vals['order_policy'] == 'picking':
|
|
vals.update({'invoice_quantity': 'procurement'})
|
|
return super(sale_order, self).write(cr, uid, ids, vals, context=context)
|
|
|
|
def create(self, cr, uid, vals, context=None):
|
|
if vals.get('order_policy', False):
|
|
if vals['order_policy'] == 'prepaid':
|
|
vals.update({'invoice_quantity': 'order'})
|
|
if vals['order_policy'] == 'picking':
|
|
vals.update({'invoice_quantity': 'procurement'})
|
|
order = super(sale_order, self).create(cr, uid, vals, context=context)
|
|
return order
|
|
|
|
# This is False
|
|
def _picked_rate(self, cr, uid, ids, name, arg, context=None):
|
|
if not ids:
|
|
return {}
|
|
res = {}
|
|
tmp = {}
|
|
for id in ids:
|
|
tmp[id] = {'picked': 0.0, 'total': 0.0}
|
|
cr.execute('''SELECT
|
|
p.sale_id as sale_order_id, sum(m.product_qty) as nbr, mp.state as procurement_state, m.state as move_state, p.type as picking_type
|
|
FROM
|
|
stock_move m
|
|
LEFT JOIN
|
|
stock_picking p on (p.id=m.picking_id)
|
|
LEFT JOIN
|
|
procurement_order mp on (mp.move_id=m.id)
|
|
WHERE
|
|
p.sale_id IN %s GROUP BY m.state, mp.state, p.sale_id, p.type''', (tuple(ids),))
|
|
|
|
for item in cr.dictfetchall():
|
|
if item['move_state'] == 'cancel':
|
|
continue
|
|
|
|
if item['picking_type'] == 'in':#this is a returned picking
|
|
tmp[item['sale_order_id']]['total'] -= item['nbr'] or 0.0 # Deducting the return picking qty
|
|
if item['procurement_state'] == 'done' or item['move_state'] == 'done':
|
|
tmp[item['sale_order_id']]['picked'] -= item['nbr'] or 0.0
|
|
else:
|
|
tmp[item['sale_order_id']]['total'] += item['nbr'] or 0.0
|
|
if item['procurement_state'] == 'done' or item['move_state'] == 'done':
|
|
tmp[item['sale_order_id']]['picked'] += item['nbr'] or 0.0
|
|
|
|
for order in self.browse(cr, uid, ids, context=context):
|
|
if order.shipped:
|
|
res[order.id] = 100.0
|
|
else:
|
|
res[order.id] = tmp[order.id]['total'] and (100.0 * tmp[order.id]['picked'] / tmp[order.id]['total']) or 0.0
|
|
return res
|
|
|
|
_columns = {
|
|
'state': fields.selection([
|
|
('draft', 'Draft Quotation'),
|
|
('sent', 'Quotation Sent'),
|
|
('cancel', 'Cancelled'),
|
|
('waiting_date', 'Waiting Schedule'),
|
|
('progress', 'Sales Order'),
|
|
('manual', 'Sale to Invoice'),
|
|
('shipping_except', 'Shipping Exception'),
|
|
('invoice_except', 'Invoice Exception'),
|
|
('done', 'Done'),
|
|
], 'Status', readonly=True,help="Gives the status of the quotation or sales order.\
|
|
\nThe exception status is automatically set when a cancel operation occurs \
|
|
in the invoice validation (Invoice Exception) or in the picking list process (Shipping Exception).\nThe 'Waiting Schedule' status is set when the invoice is confirmed\
|
|
but waiting for the scheduler to run on the order date.", select=True),
|
|
'incoterm': fields.many2one('stock.incoterms', 'Incoterm', help="International Commercial Terms are a series of predefined commercial terms used in international transactions."),
|
|
'picking_policy': fields.selection([('direct', 'Deliver each product when available'), ('one', 'Deliver all products at once')],
|
|
'Shipping Policy', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
|
|
help="""Pick 'Deliver each product when available' if you allow partial delivery."""),
|
|
'order_policy': fields.selection([
|
|
('manual', 'On Demand'),
|
|
('picking', 'On Delivery Order'),
|
|
('prepaid', 'Before Delivery'),
|
|
], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
|
|
help="""On demand: A draft invoice can be created from the sales order when needed. \nOn delivery order: A draft invoice can be created from the delivery order when the products have been delivered. \nBefore delivery: A draft invoice is created from the sales order and must be paid before the products can be delivered."""),
|
|
'picking_ids': fields.one2many('stock.picking.out', 'sale_id', 'Related Picking', readonly=True, help="This is a list of delivery orders that has been generated for this sales order."),
|
|
'shipped': fields.boolean('Delivered', readonly=True, help="It indicates that the sales order has been delivered. This field is updated only after the scheduler(s) have been launched."),
|
|
'picked_rate': fields.function(_picked_rate, string='Picked', type='float'),
|
|
'invoice_quantity': fields.selection([('order', 'Ordered Quantities'), ('procurement', 'Shipped Quantities')], 'Invoice on',
|
|
help="The sales order will automatically create the invoice proposition (draft invoice).\
|
|
You have to choose if you want your invoice based on ordered ", required=True, readonly=True, states={'draft': [('readonly', False)]}),
|
|
}
|
|
_defaults = {
|
|
'picking_policy': 'direct',
|
|
'order_policy': 'manual',
|
|
'invoice_quantity': 'order',
|
|
}
|
|
|
|
# Form filling
|
|
def unlink(self, cr, uid, ids, context=None):
|
|
sale_orders = self.read(cr, uid, ids, ['state'], context=context)
|
|
unlink_ids = []
|
|
for s in sale_orders:
|
|
if s['state'] in ['draft', 'cancel']:
|
|
unlink_ids.append(s['id'])
|
|
else:
|
|
raise osv.except_osv(_('Invalid Action!'), _('In order to delete a confirmed sales order, you must cancel it.\nTo do so, you must first cancel related picking for delivery orders.'))
|
|
|
|
return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
|
|
|
|
def action_view_delivery(self, cr, uid, ids, context=None):
|
|
'''
|
|
This function returns an action that display existing delivery orders of given sales order ids. It can either be a in a list or in a form view, if there is only one delivery order to show.
|
|
'''
|
|
mod_obj = self.pool.get('ir.model.data')
|
|
act_obj = self.pool.get('ir.actions.act_window')
|
|
|
|
result = mod_obj.get_object_reference(cr, uid, 'stock', 'action_picking_tree')
|
|
id = result and result[1] or False
|
|
result = act_obj.read(cr, uid, [id], context=context)[0]
|
|
#compute the number of delivery orders to display
|
|
pick_ids = []
|
|
for so in self.browse(cr, uid, ids, context=context):
|
|
pick_ids += [picking.id for picking in so.picking_ids]
|
|
#choose the view_mode accordingly
|
|
if len(pick_ids) > 1:
|
|
result['domain'] = "[('id','in',["+','.join(map(str, pick_ids))+"])]"
|
|
else:
|
|
res = mod_obj.get_object_reference(cr, uid, 'stock', 'view_picking_out_form')
|
|
result['views'] = [(res and res[1] or False, 'form')]
|
|
result['res_id'] = pick_ids and pick_ids[0] or False
|
|
return result
|
|
|
|
def action_invoice_create(self, cr, uid, ids, grouped=False, states=['confirmed', 'done', 'exception'], date_invoice = False, context=None):
|
|
picking_obj = self.pool.get('stock.picking')
|
|
res = super(sale_order,self).action_invoice_create( cr, uid, ids, grouped=grouped, states=states, date_invoice = date_invoice, context=context)
|
|
for order in self.browse(cr, uid, ids, context=context):
|
|
if order.order_policy == 'picking':
|
|
picking_obj.write(cr, uid, map(lambda x: x.id, order.picking_ids), {'invoice_state': 'invoiced'})
|
|
return res
|
|
|
|
def action_cancel(self, cr, uid, ids, context=None):
|
|
wf_service = netsvc.LocalService("workflow")
|
|
if context is None:
|
|
context = {}
|
|
sale_order_line_obj = self.pool.get('sale.order.line')
|
|
proc_obj = self.pool.get('procurement.order')
|
|
for sale in self.browse(cr, uid, ids, context=context):
|
|
for pick in sale.picking_ids:
|
|
if pick.state not in ('draft', 'cancel'):
|
|
raise osv.except_osv(
|
|
_('Cannot cancel sales order!'),
|
|
_('You must first cancel all delivery order(s) attached to this sales order.'))
|
|
if pick.state == 'cancel':
|
|
for mov in pick.move_lines:
|
|
proc_ids = proc_obj.search(cr, uid, [('move_id', '=', mov.id)])
|
|
if proc_ids:
|
|
for proc in proc_ids:
|
|
wf_service.trg_validate(uid, 'procurement.order', proc, 'button_check', cr)
|
|
for r in self.read(cr, uid, ids, ['picking_ids']):
|
|
for pick in r['picking_ids']:
|
|
wf_service.trg_validate(uid, 'stock.picking', pick, 'button_cancel', cr)
|
|
return super(sale_order, self).action_cancel(cr, uid, ids, context=context)
|
|
|
|
def action_wait(self, cr, uid, ids, context=None):
|
|
res = super(sale_order, self).action_wait(cr, uid, ids, context=context)
|
|
for o in self.browse(cr, uid, ids):
|
|
noprod = self.test_no_product(cr, uid, o, context)
|
|
if noprod and o.order_policy=='picking':
|
|
self.write(cr, uid, [o.id], {'order_policy': 'manual'}, context=context)
|
|
return res
|
|
|
|
def procurement_lines_get(self, cr, uid, ids, *args):
|
|
res = []
|
|
for order in self.browse(cr, uid, ids, context={}):
|
|
for line in order.order_line:
|
|
if line.procurement_id:
|
|
res.append(line.procurement_id.id)
|
|
return res
|
|
|
|
def date_to_datetime(self, cr, uid, userdate, context=None):
|
|
""" Convert date values expressed in user's timezone to
|
|
server-side UTC timestamp, assuming a default arbitrary
|
|
time of 12:00 AM - because a time is needed.
|
|
|
|
:param str userdate: date string in in user time zone
|
|
:return: UTC datetime string for server-side use
|
|
"""
|
|
# TODO: move to fields.datetime in server after 7.0
|
|
user_date = datetime.strptime(userdate, DEFAULT_SERVER_DATE_FORMAT)
|
|
if context and context.get('tz'):
|
|
tz_name = context['tz']
|
|
else:
|
|
tz_name = self.pool.get('res.users').read(cr, SUPERUSER_ID, uid, ['tz'])['tz']
|
|
if tz_name:
|
|
utc = pytz.timezone('UTC')
|
|
context_tz = pytz.timezone(tz_name)
|
|
user_datetime = user_date + relativedelta(hours=12.0)
|
|
local_timestamp = context_tz.localize(user_datetime, is_dst=False)
|
|
user_datetime = local_timestamp.astimezone(utc)
|
|
return user_datetime.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
|
|
return user_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
|
|
|
|
# if mode == 'finished':
|
|
# returns True if all lines are done, False otherwise
|
|
# if mode == 'canceled':
|
|
# returns True if there is at least one canceled line, False otherwise
|
|
def test_state(self, cr, uid, ids, mode, *args):
|
|
assert mode in ('finished', 'canceled'), _("invalid mode for test_state")
|
|
finished = True
|
|
canceled = False
|
|
write_done_ids = []
|
|
write_cancel_ids = []
|
|
for order in self.browse(cr, uid, ids, context={}):
|
|
for line in order.order_line:
|
|
if (not line.procurement_id) or (line.procurement_id.state=='done'):
|
|
if line.state != 'done':
|
|
write_done_ids.append(line.id)
|
|
else:
|
|
finished = False
|
|
if line.procurement_id:
|
|
if (line.procurement_id.state == 'cancel'):
|
|
canceled = True
|
|
if line.state != 'exception':
|
|
write_cancel_ids.append(line.id)
|
|
if write_done_ids:
|
|
self.pool.get('sale.order.line').write(cr, uid, write_done_ids, {'state': 'done'})
|
|
if write_cancel_ids:
|
|
self.pool.get('sale.order.line').write(cr, uid, write_cancel_ids, {'state': 'exception'})
|
|
|
|
if mode == 'finished':
|
|
return finished
|
|
elif mode == 'canceled':
|
|
return canceled
|
|
|
|
def _prepare_order_line_procurement(self, cr, uid, order, line, move_id, date_planned, context=None):
|
|
return {
|
|
'name': line.name,
|
|
'origin': order.name,
|
|
'date_planned': date_planned,
|
|
'product_id': line.product_id.id,
|
|
'product_qty': line.product_uom_qty,
|
|
'product_uom': line.product_uom.id,
|
|
'product_uos_qty': (line.product_uos and line.product_uos_qty)\
|
|
or line.product_uom_qty,
|
|
'product_uos': (line.product_uos and line.product_uos.id)\
|
|
or line.product_uom.id,
|
|
'location_id': order.shop_id.warehouse_id.lot_stock_id.id,
|
|
'procure_method': line.type,
|
|
'move_id': move_id,
|
|
'company_id': order.company_id.id,
|
|
'note': line.name,
|
|
}
|
|
|
|
def _prepare_order_line_move(self, cr, uid, order, line, picking_id, date_planned, context=None):
|
|
location_id = order.shop_id.warehouse_id.lot_stock_id.id
|
|
output_id = order.shop_id.warehouse_id.lot_output_id.id
|
|
return {
|
|
'name': line.name,
|
|
'picking_id': picking_id,
|
|
'product_id': line.product_id.id,
|
|
'date': date_planned,
|
|
'date_expected': date_planned,
|
|
'product_qty': line.product_uom_qty,
|
|
'product_uom': line.product_uom.id,
|
|
'product_uos_qty': (line.product_uos and line.product_uos_qty) or line.product_uom_qty,
|
|
'product_uos': (line.product_uos and line.product_uos.id)\
|
|
or line.product_uom.id,
|
|
'product_packaging': line.product_packaging.id,
|
|
'partner_id': line.address_allotment_id.id or order.partner_shipping_id.id,
|
|
'location_id': location_id,
|
|
'location_dest_id': output_id,
|
|
'sale_line_id': line.id,
|
|
'tracking_id': False,
|
|
'state': 'draft',
|
|
#'state': 'waiting',
|
|
'company_id': order.company_id.id,
|
|
'price_unit': line.product_id.standard_price or 0.0
|
|
}
|
|
|
|
def _prepare_order_picking(self, cr, uid, order, context=None):
|
|
pick_name = self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.out')
|
|
return {
|
|
'name': pick_name,
|
|
'origin': order.name,
|
|
'date': self.date_to_datetime(cr, uid, order.date_order, context),
|
|
'type': 'out',
|
|
'state': 'auto',
|
|
'move_type': order.picking_policy,
|
|
'sale_id': order.id,
|
|
'partner_id': order.partner_shipping_id.id,
|
|
'note': order.note,
|
|
'invoice_state': (order.order_policy=='picking' and '2binvoiced') or 'none',
|
|
'company_id': order.company_id.id,
|
|
}
|
|
|
|
def ship_recreate(self, cr, uid, order, line, move_id, proc_id):
|
|
# FIXME: deals with potentially cancelled shipments, seems broken (specially if shipment has production lot)
|
|
"""
|
|
Define ship_recreate for process after shipping exception
|
|
param order: sales order to which the order lines belong
|
|
param line: sales order line records to procure
|
|
param move_id: the ID of stock move
|
|
param proc_id: the ID of procurement
|
|
"""
|
|
move_obj = self.pool.get('stock.move')
|
|
if order.state == 'shipping_except':
|
|
for pick in order.picking_ids:
|
|
for move in pick.move_lines:
|
|
if move.state == 'cancel':
|
|
mov_ids = move_obj.search(cr, uid, [('state', '=', 'cancel'),('sale_line_id', '=', line.id),('picking_id', '=', pick.id)])
|
|
if mov_ids:
|
|
for mov in move_obj.browse(cr, uid, mov_ids):
|
|
# FIXME: the following seems broken: what if move_id doesn't exist? What if there are several mov_ids? Shouldn't that be a sum?
|
|
move_obj.write(cr, uid, [move_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
|
|
self.pool.get('procurement.order').write(cr, uid, [proc_id], {'product_qty': mov.product_qty, 'product_uos_qty': mov.product_uos_qty})
|
|
return True
|
|
|
|
def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
|
|
start_date = self.date_to_datetime(cr, uid, start_date, context)
|
|
date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATETIME_FORMAT) + relativedelta(days=line.delay or 0.0)
|
|
date_planned = (date_planned - timedelta(days=order.company_id.security_lead)).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
|
|
return date_planned
|
|
|
|
def _create_pickings_and_procurements(self, cr, uid, order, order_lines, picking_id=False, context=None):
|
|
"""Create the required procurements to supply sales order lines, also connecting
|
|
the procurements to appropriate stock moves in order to bring the goods to the
|
|
sales order's requested location.
|
|
|
|
If ``picking_id`` is provided, the stock moves will be added to it, otherwise
|
|
a standard outgoing picking will be created to wrap the stock moves, as returned
|
|
by :meth:`~._prepare_order_picking`.
|
|
|
|
Modules that wish to customize the procurements or partition the stock moves over
|
|
multiple stock pickings may override this method and call ``super()`` with
|
|
different subsets of ``order_lines`` and/or preset ``picking_id`` values.
|
|
|
|
:param browse_record order: sales order to which the order lines belong
|
|
:param list(browse_record) order_lines: sales order line records to procure
|
|
:param int picking_id: optional ID of a stock picking to which the created stock moves
|
|
will be added. A new picking will be created if ommitted.
|
|
:return: True
|
|
"""
|
|
move_obj = self.pool.get('stock.move')
|
|
picking_obj = self.pool.get('stock.picking')
|
|
procurement_obj = self.pool.get('procurement.order')
|
|
proc_ids = []
|
|
|
|
for line in order_lines:
|
|
if line.state == 'done':
|
|
continue
|
|
|
|
date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context)
|
|
|
|
if line.product_id:
|
|
if line.product_id.type in ('product', 'consu'):
|
|
if not picking_id:
|
|
picking_id = picking_obj.create(cr, uid, self._prepare_order_picking(cr, uid, order, context=context))
|
|
move_id = move_obj.create(cr, uid, self._prepare_order_line_move(cr, uid, order, line, picking_id, date_planned, context=context))
|
|
else:
|
|
# a service has no stock move
|
|
move_id = False
|
|
|
|
proc_id = procurement_obj.create(cr, uid, self._prepare_order_line_procurement(cr, uid, order, line, move_id, date_planned, context=context))
|
|
proc_ids.append(proc_id)
|
|
line.write({'procurement_id': proc_id})
|
|
self.ship_recreate(cr, uid, order, line, move_id, proc_id)
|
|
|
|
wf_service = netsvc.LocalService("workflow")
|
|
if picking_id:
|
|
wf_service.trg_validate(uid, 'stock.picking', picking_id, 'button_confirm', cr)
|
|
for proc_id in proc_ids:
|
|
wf_service.trg_validate(uid, 'procurement.order', proc_id, 'button_confirm', cr)
|
|
|
|
val = {}
|
|
if order.state == 'shipping_except':
|
|
val['state'] = 'progress'
|
|
val['shipped'] = False
|
|
|
|
if (order.order_policy == 'manual'):
|
|
for line in order.order_line:
|
|
if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
|
|
val['state'] = 'manual'
|
|
break
|
|
order.write(val)
|
|
return True
|
|
|
|
def action_ship_create(self, cr, uid, ids, context=None):
|
|
for order in self.browse(cr, uid, ids, context=context):
|
|
self._create_pickings_and_procurements(cr, uid, order, order.order_line, None, context=context)
|
|
return True
|
|
|
|
def action_ship_end(self, cr, uid, ids, context=None):
|
|
for order in self.browse(cr, uid, ids, context=context):
|
|
val = {'shipped': True}
|
|
if order.state == 'shipping_except':
|
|
val['state'] = 'progress'
|
|
if (order.order_policy == 'manual'):
|
|
for line in order.order_line:
|
|
if (not line.invoiced) and (line.state not in ('cancel', 'draft')):
|
|
val['state'] = 'manual'
|
|
break
|
|
for line in order.order_line:
|
|
towrite = []
|
|
if line.state == 'exception':
|
|
towrite.append(line.id)
|
|
if towrite:
|
|
self.pool.get('sale.order.line').write(cr, uid, towrite, {'state': 'done'}, context=context)
|
|
res = self.write(cr, uid, [order.id], val)
|
|
return True
|
|
|
|
def has_stockable_products(self, cr, uid, ids, *args):
|
|
for order in self.browse(cr, uid, ids):
|
|
for order_line in order.order_line:
|
|
if order_line.product_id and order_line.product_id.type in ('product', 'consu'):
|
|
return True
|
|
return False
|
|
|
|
|
|
class sale_order_line(osv.osv):
|
|
|
|
def _number_packages(self, cr, uid, ids, field_name, arg, context=None):
|
|
res = {}
|
|
for line in self.browse(cr, uid, ids, context=context):
|
|
try:
|
|
res[line.id] = int((line.product_uom_qty+line.product_packaging.qty-0.0001) / line.product_packaging.qty)
|
|
except:
|
|
res[line.id] = 1
|
|
return res
|
|
|
|
_inherit = 'sale.order.line'
|
|
_columns = {
|
|
'delay': fields.float('Delivery Lead Time', required=True, help="Number of days between the order confirmation and the shipping of the products to the customer", readonly=True, states={'draft': [('readonly', False)]}),
|
|
'procurement_id': fields.many2one('procurement.order', 'Procurement'),
|
|
'property_ids': fields.many2many('mrp.property', 'sale_order_line_property_rel', 'order_id', 'property_id', 'Properties', readonly=True, states={'draft': [('readonly', False)]}),
|
|
'product_packaging': fields.many2one('product.packaging', 'Packaging'),
|
|
'move_ids': fields.one2many('stock.move', 'sale_line_id', 'Inventory Moves', readonly=True),
|
|
'number_packages': fields.function(_number_packages, type='integer', string='Number Packages'),
|
|
}
|
|
_defaults = {
|
|
'delay': 0.0,
|
|
'product_packaging': False,
|
|
}
|
|
|
|
def _get_line_qty(self, cr, uid, line, context=None):
|
|
if line.procurement_id and not (line.order_id.invoice_quantity=='order'):
|
|
return self.pool.get('procurement.order').quantity_get(cr, uid,
|
|
line.procurement_id.id, context=context)
|
|
else:
|
|
return super(sale_order_line, self)._get_line_qty(cr, uid, line, context=context)
|
|
|
|
|
|
def _get_line_uom(self, cr, uid, line, context=None):
|
|
if line.procurement_id and not (line.order_id.invoice_quantity=='order'):
|
|
return self.pool.get('procurement.order').uom_get(cr, uid,
|
|
line.procurement_id.id, context=context)
|
|
else:
|
|
return super(sale_order_line, self)._get_line_uom(cr, uid, line, context=context)
|
|
|
|
def button_cancel(self, cr, uid, ids, context=None):
|
|
res = super(sale_order_line, self).button_cancel(cr, uid, ids, context=context)
|
|
for line in self.browse(cr, uid, ids, context=context):
|
|
for move_line in line.move_ids:
|
|
if move_line.state != 'cancel':
|
|
raise osv.except_osv(
|
|
_('Cannot cancel sales order line!'),
|
|
_('You must first cancel stock moves attached to this sales order line.'))
|
|
return res
|
|
|
|
def copy_data(self, cr, uid, id, default=None, context=None):
|
|
if not default:
|
|
default = {}
|
|
default.update({'move_ids': []})
|
|
return super(sale_order_line, self).copy_data(cr, uid, id, default, context=context)
|
|
|
|
def product_packaging_change(self, cr, uid, ids, pricelist, product, qty=0, uom=False,
|
|
partner_id=False, packaging=False, flag=False, context=None):
|
|
if not product:
|
|
return {'value': {'product_packaging': False}}
|
|
product_obj = self.pool.get('product.product')
|
|
product_uom_obj = self.pool.get('product.uom')
|
|
pack_obj = self.pool.get('product.packaging')
|
|
warning = {}
|
|
result = {}
|
|
warning_msgs = ''
|
|
if flag:
|
|
res = self.product_id_change(cr, uid, ids, pricelist=pricelist,
|
|
product=product, qty=qty, uom=uom, partner_id=partner_id,
|
|
packaging=packaging, flag=False, context=context)
|
|
warning_msgs = res.get('warning') and res['warning']['message']
|
|
|
|
products = product_obj.browse(cr, uid, product, context=context)
|
|
if not products.packaging:
|
|
packaging = result['product_packaging'] = False
|
|
elif not packaging and products.packaging and not flag:
|
|
packaging = products.packaging[0].id
|
|
result['product_packaging'] = packaging
|
|
|
|
if packaging:
|
|
default_uom = products.uom_id and products.uom_id.id
|
|
pack = pack_obj.browse(cr, uid, packaging, context=context)
|
|
q = product_uom_obj._compute_qty(cr, uid, uom, pack.qty, default_uom)
|
|
# qty = qty - qty % q + q
|
|
if qty and (q and not (qty % q) == 0):
|
|
ean = pack.ean or _('(n/a)')
|
|
qty_pack = pack.qty
|
|
type_ul = pack.ul
|
|
if not warning_msgs:
|
|
warn_msg = _("You selected a quantity of %d Units.\n"
|
|
"But it's not compatible with the selected packaging.\n"
|
|
"Here is a proposition of quantities according to the packaging:\n"
|
|
"EAN: %s Quantity: %s Type of ul: %s") % \
|
|
(qty, ean, qty_pack, type_ul.name)
|
|
warning_msgs += _("Picking Information ! : ") + warn_msg + "\n\n"
|
|
warning = {
|
|
'title': _('Configuration Error!'),
|
|
'message': warning_msgs
|
|
}
|
|
result['product_uom_qty'] = qty
|
|
|
|
return {'value': result, 'warning': warning}
|
|
|
|
def product_id_change(self, cr, uid, ids, pricelist, product, qty=0,
|
|
uom=False, qty_uos=0, uos=False, name='', partner_id=False,
|
|
lang=False, update_tax=True, date_order=False, packaging=False, fiscal_position=False, flag=False, context=None):
|
|
context = context or {}
|
|
product_uom_obj = self.pool.get('product.uom')
|
|
partner_obj = self.pool.get('res.partner')
|
|
product_obj = self.pool.get('product.product')
|
|
warning = {}
|
|
res = super(sale_order_line, self).product_id_change(cr, uid, ids, pricelist, product, qty=qty,
|
|
uom=uom, qty_uos=qty_uos, uos=uos, name=name, partner_id=partner_id,
|
|
lang=lang, update_tax=update_tax, date_order=date_order, packaging=packaging, fiscal_position=fiscal_position, flag=flag, context=context)
|
|
|
|
if not product:
|
|
res['value'].update({'product_packaging': False})
|
|
return res
|
|
|
|
#update of result obtained in super function
|
|
product_obj = product_obj.browse(cr, uid, product, context=context)
|
|
res['value']['delay'] = (product_obj.sale_delay or 0.0)
|
|
res['value']['type'] = product_obj.procure_method
|
|
|
|
#check if product is available, and if not: raise an error
|
|
uom2 = False
|
|
if uom:
|
|
uom2 = product_uom_obj.browse(cr, uid, uom, context=context)
|
|
if product_obj.uom_id.category_id.id != uom2.category_id.id:
|
|
uom = False
|
|
if not uom2:
|
|
uom2 = product_obj.uom_id
|
|
|
|
# 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 ''
|
|
compare_qty = float_compare(product_obj.virtual_available * uom2.factor, qty * product_obj.uom_id.factor, precision_rounding=product_obj.uom_id.rounding)
|
|
if (product_obj.type=='product') and int(compare_qty) == -1 \
|
|
and (product_obj.procure_method=='make_to_stock'):
|
|
warn_msg = _('You plan to sell %.2f %s but you only have %.2f %s available !\nThe real stock is %.2f %s. (without reservations)') % \
|
|
(qty, uom2 and uom2.name or product_obj.uom_id.name,
|
|
max(0,product_obj.virtual_available), product_obj.uom_id.name,
|
|
max(0,product_obj.qty_available), product_obj.uom_id.name)
|
|
warning_msgs += _("Not enough stock ! : ") + warn_msg + "\n\n"
|
|
|
|
#update of warning messages
|
|
if warning_msgs:
|
|
warning = {
|
|
'title': _('Configuration Error!'),
|
|
'message' : warning_msgs
|
|
}
|
|
res.update({'warning': warning})
|
|
return res
|
|
|
|
|
|
class sale_advance_payment_inv(osv.osv_memory):
|
|
_inherit = "sale.advance.payment.inv"
|
|
|
|
def _create_invoices(self, cr, uid, inv_values, sale_id, context=None):
|
|
result = super(sale_advance_payment_inv, self)._create_invoices(cr, uid, inv_values, sale_id, context=context)
|
|
sale_obj = self.pool.get('sale.order')
|
|
sale_line_obj = self.pool.get('sale.order.line')
|
|
wizard = self.browse(cr, uid, [result], context)
|
|
sale = sale_obj.browse(cr, uid, sale_id, context=context)
|
|
|
|
# If invoice on picking: add the cost on the SO
|
|
# If not, the advance will be deduced when generating the final invoice
|
|
line_name = inv_values.get('invoice_line') and inv_values.get('invoice_line')[0][2].get('name') or ''
|
|
line_tax = inv_values.get('invoice_line') and inv_values.get('invoice_line')[0][2].get('invoice_line_tax_id') or False
|
|
if sale.order_policy == 'picking':
|
|
vals = {
|
|
'order_id': sale.id,
|
|
'name': line_name,
|
|
'price_unit': -inv_amount,
|
|
'product_uom_qty': wizard.qtty or 1.0,
|
|
'product_uos_qty': wizard.qtty or 1.0,
|
|
'product_uos': res.get('uos_id', False),
|
|
'product_uom': res.get('uom_id', False),
|
|
'product_id': wizard.product_id.id or False,
|
|
'discount': False,
|
|
'tax_id': line_tax,
|
|
}
|
|
sale_line_obj.create(cr, uid, vals, context=context)
|
|
return result
|