diff --git a/addons/sale/sale.py b/addons/sale/sale.py
index 32ac3d8e1af..b8a54a6faa2 100644
--- a/addons/sale/sale.py
+++ b/addons/sale/sale.py
@@ -2,561 +2,307 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, timedelta
-import time
from openerp import SUPERUSER_ID
-from openerp.addons.analytic.models import analytic
-from openerp.osv import fields, osv
-from openerp.tools.translate import _
-from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
+from openerp import api, fields, models, _
import openerp.addons.decimal_precision as dp
-from openerp import workflow
from openerp.exceptions import UserError
+from openerp.tools import float_is_zero, float_compare, DEFAULT_SERVER_DATETIME_FORMAT
-class res_company(osv.Model):
+
+class res_company(models.Model):
_inherit = "res.company"
- _columns = {
- 'sale_note': fields.text('Default Terms and Conditions', translate=True, help="Default terms and conditions for quotations."),
- }
+ sale_note = fields.Text(string='Default Terms and Conditions', translate=True)
-class sale_order(osv.osv):
+
+class SaleOrder(models.Model):
_name = "sale.order"
_inherit = ['mail.thread', 'ir.needaction_mixin']
_description = "Sales Order"
-
- def _amount_line_tax(self, cr, uid, line, context=None):
- val = 0.0
- if line.tax_id.ids:
- for c in self.pool.get('account.tax').compute_all(cr, uid, line.tax_id.ids, line.price_unit * (1-(line.discount or 0.0)/100.0), line.order_id.currency_id.id, line.product_uom_qty, line.product_id.id, line.order_id.partner_id.id)['taxes']:
- val += c.get('amount', 0.0)
- return val
-
- def _amount_all_wrapper(self, cr, uid, ids, field_name, arg, context=None):
- """ Wrapper because of direct method passing as parameter for function fields """
- return self._amount_all(cr, uid, ids, field_name, arg, context=context)
-
- def _amount_all(self, cr, uid, ids, field_name, arg, context=None):
- cur_obj = self.pool.get('res.currency')
- res = {}
- for order in self.browse(cr, uid, ids, context=context):
- res[order.id] = {
- 'amount_untaxed': 0.0,
- 'amount_tax': 0.0,
- 'amount_total': 0.0,
- }
- val = val1 = 0.0
- cur = order.pricelist_id.currency_id
- for line in order.order_line:
- val1 += line.price_subtotal
- val += self._amount_line_tax(cr, uid, line, context=context)
- res[order.id]['amount_tax'] = cur_obj.round(cr, uid, cur, val)
- res[order.id]['amount_untaxed'] = cur_obj.round(cr, uid, cur, val1)
- res[order.id]['amount_total'] = res[order.id]['amount_untaxed'] + res[order.id]['amount_tax']
- return res
-
- def _search_invoiced(self, cursor, user, obj, name, args, context=None):
- if not len(args):
- return []
- clause = ''
- sale_clause = ''
- no_invoiced = False
- for arg in args:
- if (arg[1] == '=' and arg[2]) or (arg[1] == '!=' and not arg[2]):
- clause += 'AND inv.state = \'paid\''
- else:
- clause += 'AND inv.state != \'cancel\' AND sale.state != \'cancel\' AND inv.state <> \'paid\' AND rel.order_id = sale.id '
- sale_clause = ', sale_order AS sale '
- no_invoiced = True
-
- cursor.execute('SELECT rel.order_id ' \
- 'FROM sale_order_invoice_rel AS rel, account_invoice AS inv '+ sale_clause + \
- 'WHERE rel.invoice_id = inv.id ' + clause)
- res = cursor.fetchall()
- if no_invoiced:
- cursor.execute('SELECT sale.id ' \
- 'FROM sale_order AS sale ' \
- 'WHERE sale.id NOT IN ' \
- '(SELECT rel.order_id ' \
- 'FROM sale_order_invoice_rel AS rel) and sale.state != \'cancel\'')
- res.extend(cursor.fetchall())
- if not res:
- return [('id', '=', 0)]
- return [('id', 'in', [x[0] for x in res])]
-
- def _get_order(self, cr, uid, ids, context=None):
- result = {}
- for line in self.pool.get('sale.order.line').browse(cr, uid, ids, context=context):
- result[line.order_id.id] = True
- return result.keys()
-
- def _get_default_company(self, cr, uid, context=None):
- company_id = self.pool.get('res.users')._get_company(cr, uid, context=context)
- if not company_id:
- raise UserError(_('There is no default company for the current user!'))
- return company_id
-
- def _get_invoiced(self, cr, uid, ids, field_name, arg, context=None):
- res = {}
- for order in self.browse(cr, uid, ids, context=context):
- res[order.id] = {
- 'invoice_count': len(order.invoice_ids),
- 'invoiced': False,
- }
- if order.state != 'manual' and any(invoice.state == 'paid' for invoice in order.invoice_ids):
- res[order.id]['invoiced'] = True
- return res
-
- _columns = {
- 'name': fields.char('Order Reference', required=True, copy=False,
- readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True),
- 'origin': fields.char('Source Document', help="Reference of the document that generated this sales order request."),
- 'client_order_ref': fields.char('Customer Reference', copy=False),
- '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, copy=False, 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),
- 'date_order': fields.datetime('Date', required=True, readonly=True, select=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, copy=False),
- 'validity_date': fields.date('Expiration Date', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}),
- 'create_date': fields.datetime('Creation Date', readonly=True, select=True, help="Date on which sales order is created."),
- 'date_confirm': fields.date('Confirmation Date', readonly=True, select=True, help="Date on which sales order is confirmed.", copy=False),
- 'user_id': fields.many2one('res.users', 'Salesperson', states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, select=True, track_visibility='onchange'),
- 'partner_id': fields.many2one('res.partner', 'Customer', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, required=True, change_default=True, select=True, track_visibility='always'),
- 'partner_invoice_id': fields.many2one('res.partner', 'Invoice Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Invoice address for current sales order."),
- 'partner_shipping_id': fields.many2one('res.partner', 'Delivery Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Delivery address for current sales order."),
- 'order_policy': fields.selection([
- ('manual', 'On Demand'),
- ], 'Create Invoice', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
- help="""This field controls how invoice and delivery operations are synchronized."""),
- 'pricelist_id': fields.many2one('product.pricelist', 'Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Pricelist for current sales order."),
- 'currency_id': fields.related('pricelist_id', 'currency_id', type="many2one", relation="res.currency", string="Currency", readonly=True, required=True),
- 'project_id': fields.many2one('account.analytic.account', 'Contract / Analytic', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="The analytic account related to a sales order."),
- 'contract_state': fields.related('project_id', 'state', string='Contract Status', type='selection', selection=analytic.ANALYTIC_ACCOUNT_STATE),
-
- 'order_line': fields.one2many('sale.order.line', 'order_id', 'Order Lines', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, copy=True),
- 'invoice_ids': fields.many2many('account.invoice', 'sale_order_invoice_rel', 'order_id', 'invoice_id', 'Invoices', readonly=True, copy=False, help="This is the list of invoices that have been generated for this sales order. The same sales order may have been invoiced in several times (by line for example)."),
- 'invoice_count': fields.function(_get_invoiced, type='integer', string='Invoices', multi="counts"),
- 'invoiced': fields.function(_get_invoiced, fnct_search=_search_invoiced, type='boolean', string='Paid', multi="counts"),
- 'note': fields.text('Terms and conditions'),
- 'amount_untaxed': fields.function(_amount_all_wrapper, digits=0, string='Untaxed Amount',
- store={
- 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
- 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
- },
- multi='sums', help="The amount without tax.", track_visibility='always'),
- 'amount_tax': fields.function(_amount_all_wrapper, digits=0, string='Taxes',
- store={
- 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
- 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
- },
- multi='sums', help="The tax amount."),
- 'amount_total': fields.function(_amount_all_wrapper, digits=0, string='Total',
- store={
- 'sale.order': (lambda self, cr, uid, ids, c={}: ids, ['order_line'], 10),
- 'sale.order.line': (_get_order, ['price_unit', 'tax_id', 'discount', 'product_uom_qty'], 10),
- },
- multi='sums', help="The total amount."),
-
- 'payment_term_id': fields.many2one('account.payment.term', string='Payment Term', oldname='payment_term'),
- 'fiscal_position_id': fields.many2one('account.fiscal.position', oldname='fiscal_position', string='Fiscal Position'),
- 'company_id': fields.many2one('res.company', 'Company'),
- 'team_id': fields.many2one('crm.team', 'Sales Team', oldname='section_id', change_default=True),
- 'procurement_group_id': fields.many2one('procurement.group', 'Procurement group', copy=False),
- 'product_id': fields.related('order_line', 'product_id', type='many2one', relation='product.product', string='Product'),
- }
-
- _defaults = {
- 'date_order': fields.datetime.now,
- 'order_policy': 'manual',
- 'company_id': _get_default_company,
- 'state': 'draft',
- 'user_id': lambda obj, cr, uid, context: uid,
- 'name': lambda obj, cr, uid, context: '/',
- 'partner_invoice_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').address_get(cr, uid, [context['partner_id']], ['invoice'])['invoice'],
- 'partner_shipping_id': lambda self, cr, uid, context: context.get('partner_id', False) and self.pool.get('res.partner').address_get(cr, uid, [context['partner_id']], ['delivery'])['delivery'],
- 'note': lambda self, cr, uid, context: self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.sale_note,
- 'team_id': lambda s, cr, uid, c: s.pool['crm.team']._get_default_team_id(cr, uid, context=c),
- }
- _sql_constraints = [
- ('name_uniq', 'unique(name, company_id)', 'Order Reference must be unique per Company!'),
- ]
_order = 'date_order desc, id desc'
- # 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'])
- elif s['state'] == 'sent':
- raise UserError(_('In order to delete already sent quotation(s), you must cancel it before!'))
+ @api.depends('order_line.product_uom_qty', 'order_line.discount', 'order_line.price_unit', 'order_line.tax_id')
+ def _amount_all(self):
+ amount_untaxed = amount_tax = 0.0
+ for line in self.order_line:
+ amount_untaxed += line.price_subtotal
+ amount_tax += line.price_tax
+ self.update({
+ 'amount_untaxed': self.pricelist_id.currency_id.round(amount_untaxed),
+ 'amount_tax': self.pricelist_id.currency_id.round(amount_tax),
+ 'amount_total': amount_untaxed + amount_tax,
+ })
+
+ @api.depends('state', 'order_line.invoice_status')
+ def _get_invoiced(self):
+ for order in self:
+ invoice_ids = order.order_line.mapped('invoice_lines').mapped('invoice_id').ids
+
+ if order.state not in ('sale', 'done'):
+ invoice_status = 'no'
+ elif any(line.invoice_status == 'to invoice' for line in order.order_line):
+ invoice_status = 'to invoice'
+ elif all(line.invoice_status == 'invoiced' for line in order.order_line):
+ invoice_status = 'invoiced'
+ elif all(line.invoice_status in ['invoiced', 'upselling'] for line in order.order_line):
+ invoice_status = 'upselling'
else:
- raise UserError(_('In order to delete a confirmed sales order, you must cancel it before!'))
+ invoice_status = 'no'
- return osv.osv.unlink(self, cr, uid, unlink_ids, context=context)
+ order.update({
+ 'invoice_count': len(set(invoice_ids)),
+ 'invoice_ids': invoice_ids,
+ 'invoice_status': invoice_status
+ })
- def copy_quotation(self, cr, uid, ids, context=None):
- id = self.copy(cr, uid, ids[0], context=context)
- view_ref = self.pool.get('ir.model.data').get_object_reference(cr, uid, 'sale', 'view_order_form')
- view_id = view_ref and view_ref[1] or False,
- return {
- 'type': 'ir.actions.act_window',
- 'name': _('Sales Order'),
- 'res_model': 'sale.order',
- 'res_id': id,
- 'view_type': 'form',
- 'view_mode': 'form',
- 'view_id': view_id,
- 'target': 'current',
- }
+ @api.model
+ def _default_note(self):
+ return self.env.user.company_id.sale_note
- def _track_subtype(self, cr, uid, ids, init_values, context=None):
- record = self.browse(cr, uid, ids[0], context=context)
- if 'state' in init_values and record.state in ['manual', 'progress']:
+ @api.model
+ def _get_default_team(self):
+ default_team_id = self.env['crm.team']._get_default_team_id()
+ return self.env['crm.team'].browse(default_team_id)
+
+ @api.onchange('fiscal_position_id')
+ def _compute_tax_id(self):
+ for order in self:
+ order.order_line._compute_tax_id()
+
+ name = fields.Char(string='Order Reference', required=True, copy=False, readonly=True, index=True, default='New')
+ origin = fields.Char(string='Source Document', help="Reference of the document that generated this sales order request.")
+ client_order_ref = fields.Char(string='Customer Reference', copy=False)
+
+ state = fields.Selection([
+ ('draft', 'Quotation'),
+ ('sent', 'Quotation Sent'),
+ ('sale', 'Sale Order'),
+ ('done', 'Done'),
+ ('cancel', 'Cancelled'),
+ ], string='Status', readonly=True, copy=False, index=True, default='draft')
+ date_order = fields.Datetime(string='Order Date', required=True, readonly=True, index=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, copy=False, default=fields.Date.context_today)
+ validity_date = fields.Date(string='Expiration Date', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]})
+ create_date = fields.Datetime(string='Creation Date', readonly=True, index=True, help="Date on which sales order is created.")
+
+ user_id = fields.Many2one('res.users', string='Salesperson', states={
+ 'draft': [('readonly', False)],
+ 'sent': [('readonly', False)]
+ }, index=True, track_visibility='onchange', default=lambda self: self.env.user)
+ partner_id = fields.Many2one('res.partner', string='Customer', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, required=True, change_default=True, index=True, track_visibility='always')
+ partner_invoice_id = fields.Many2one('res.partner', string='Invoice Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Invoice address for current sales order.")
+ partner_shipping_id = fields.Many2one('res.partner', string='Delivery Address', readonly=True, required=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Delivery address for current sales order.")
+
+ pricelist_id = fields.Many2one('product.pricelist', string='Pricelist', required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="Pricelist for current sales order.")
+ currency_id = fields.Many2one("res.currency", related='pricelist_id.currency_id', string="Currency", readonly=True, required=True)
+ project_id = fields.Many2one('account.analytic.account', 'Analytic Account', readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]}, help="The analytic account related to a sales order.", copy=False)
+
+ order_line = fields.One2many('sale.order.line', 'order_id', string='Order Lines', states={'cancel': [('readonly', True)], 'done': [('readonly', True)]}, copy=True)
+
+ invoice_count = fields.Integer(string='# of Invoices', compute='_get_invoiced', store=True, readonly=True)
+ invoice_ids = fields.Many2many("account.invoice", string='Invoices', compute="_get_invoiced", readonly=True, copy=False)
+ invoice_status = fields.Selection([
+ ('upselling', 'Upselling Opportunity'),
+ ('invoiced', 'Fully Invoiced'),
+ ('to invoice', 'To Invoice'),
+ ('no', 'Nothing to Invoice')
+ ], string='Invoice Status', compute='_get_invoiced', store=True, readonly=True, default='no')
+
+ note = fields.Text('Terms and conditions', default=_default_note)
+
+ amount_untaxed = fields.Monetary(string='Untaxed Amount', store=True, readonly=True, compute='_amount_all', track_visibility='always')
+ amount_tax = fields.Monetary(string='Taxes', store=True, readonly=True, compute='_amount_all', track_visibility='always')
+ amount_total = fields.Monetary(string='Total', store=True, readonly=True, compute='_amount_all', track_visibility='always')
+
+ payment_term_id = fields.Many2one('account.payment.term', string='Payment Term', oldname='payment_term')
+ fiscal_position_id = fields.Many2one('account.fiscal.position', oldname='fiscal_position', string='Fiscal Position')
+ company_id = fields.Many2one('res.company', 'Company', default=lambda self: self.env['res.company']._company_default_get('sale.order'))
+ team_id = fields.Many2one('crm.team', 'Sales Team', change_default=True, default=_get_default_team)
+ procurement_group_id = fields.Many2one('procurement.group', 'Procurement Group', copy=False)
+
+ product_id = fields.Many2one('product.product', related='order_line.product_id', string='Product')
+
+ @api.multi
+ def unlink(self):
+ for order in self:
+ if order.state != 'draft':
+ raise UserError(_('You can only delete draft quotations!'))
+ return super(SaleOrder, self).unlink()
+
+ @api.multi
+ def _track_subtype(self, init_values):
+ self.ensure_one()
+ if 'state' in init_values and self.state == 'sale':
return 'sale.mt_order_confirmed'
- elif 'state' in init_values and record.state == 'sent':
+ elif 'state' in init_values and self.state == 'sent':
return 'sale.mt_order_sent'
- return super(sale_order, self)._track_subtype(cr, uid, ids, init_values, context=context)
+ return super(SaleOrder, self)._track_subtype(init_values)
- def onchange_pricelist_id(self, cr, uid, ids, pricelist_id, order_lines, context=None):
- if not pricelist_id:
- return {}
- pricelist = self.pool['product.pricelist'].browse(cr, uid, pricelist_id, context=context)
- return {'value': {'currency_id': pricelist.currency_id.id}}
- def get_salenote(self, cr, uid, ids, partner_id, context=None):
- context_lang = context.copy()
- if partner_id:
- partner_lang = self.pool.get('res.partner').browse(cr, uid, partner_id, context=context).lang
- context_lang.update({'lang': partner_lang})
- return self.pool.get('res.users').browse(cr, uid, uid, context=context_lang).company_id.sale_note
-
- def onchange_delivery_id(self, cr, uid, ids, company_id, partner_id, delivery_id, fiscal_position_id, context=None):
- r = {'value': {}}
- if not company_id:
- company_id = self._get_default_company(cr, uid, context=context)
- fiscal_position = self.pool['account.fiscal.position'].get_fiscal_position(cr, uid, partner_id, delivery_id, context=context)
+ @api.onchange('partner_shipping_id')
+ def onchange_partner_shipping_id(self):
+ fiscal_position = self.env['account.fiscal.position'].get_fiscal_position(self.partner_id.id, self.partner_shipping_id.id)
if fiscal_position:
- r['value']['fiscal_position_id'] = fiscal_position
- return r
-
- def onchange_partner_id(self, cr, uid, ids, part, context=None):
- if not part:
- return {'value': {'partner_invoice_id': False, 'partner_shipping_id': False, 'payment_term_id': False, 'fiscal_position_id': False}}
-
- part = self.pool.get('res.partner').browse(cr, uid, part, context=context)
- addr = self.pool.get('res.partner').address_get(cr, uid, [part.id], ['delivery', 'invoice', 'contact'], context=context)
- pricelist = part.property_product_pricelist and part.property_product_pricelist.id or False
- invoice_part = self.pool.get('res.partner').browse(cr, uid, addr['invoice'], context=context)
- payment_term = invoice_part.property_payment_term_id and invoice_part.property_payment_term_id.id or False
- dedicated_salesman = part.user_id and part.user_id.id or uid
- val = {
- 'partner_invoice_id': addr['invoice'],
- 'partner_shipping_id': addr['delivery'],
- 'payment_term_id': payment_term,
- 'user_id': dedicated_salesman,
- }
- delivery_onchange = self.onchange_delivery_id(cr, uid, ids, False, part.id, addr['delivery'], False, context=context)
- val.update(delivery_onchange['value'])
- if pricelist:
- val['pricelist_id'] = pricelist
- if not self.pool['crm.team']._get_default_team_id(cr, uid, context=context) and part.team_id:
- val['team_id'] = part.team_id.id
- sale_note = self.get_salenote(cr, uid, ids, part.id, context=context)
- if sale_note: val.update({'note': sale_note})
- return {'value': val}
-
- def create(self, cr, uid, vals, context=None):
- if context is None:
- context = {}
- if vals.get('name', '/') == '/':
- vals['name'] = self.pool.get('ir.sequence').next_by_code(cr, uid, 'sale.order', context=context) or '/'
- if vals.get('partner_id') and any(f not in vals for f in ['partner_invoice_id', 'partner_shipping_id', 'pricelist_id', 'fiscal_position_id']):
- defaults = self.onchange_partner_id(cr, uid, [], vals['partner_id'], context=context)['value']
- if not vals.get('fiscal_position_id') and vals.get('partner_shipping_id'):
- delivery_onchange = self.onchange_delivery_id(cr, uid, [], vals.get('company_id'), None, vals['partner_id'], vals.get('partner_shipping_id'), context=context)
- defaults.update(delivery_onchange['value'])
- vals = dict(defaults, **vals)
- ctx = dict(context or {}, mail_create_nolog=True)
- new_id = super(sale_order, self).create(cr, uid, vals, context=ctx)
- self.message_post(cr, uid, [new_id], body=_("Quotation created"), context=ctx)
- return new_id
-
- def button_dummy(self, cr, uid, ids, context=None):
- return True
-
- # FIXME: deprecated method, overriders should be using _prepare_invoice() instead.
- # can be removed after 6.1.
- def _inv_get(self, cr, uid, order, context=None):
+ self.fiscal_position_id = fiscal_position
return {}
- def _prepare_invoice(self, cr, uid, order, lines, context=None):
+ @api.multi
+ @api.onchange('partner_id')
+ def onchange_partner_id(self):
+ if not self.partner_id:
+ self.update({
+ 'partner_invoice_id': False,
+ 'partner_shipping_id': False,
+ 'payment_term_id': False,
+ 'fiscal_position_id': False,
+ })
+ return
+
+ addr = self.partner_id.address_get(['delivery', 'invoice'])
+ values = {
+ 'pricelist_id': self.partner_id.property_product_pricelist and self.partner_id.property_product_pricelist.id or False,
+ 'payment_term_id': self.partner_id.property_payment_term_id and self.partner_id.property_payment_term_id.id or False,
+ 'partner_invoice_id': addr['invoice'],
+ 'partner_shipping_id': addr['delivery'],
+ }
+
+ if self.partner_id.user_id:
+ values['user_id'] = self.partner_id.user_id.id
+ if self.partner_id.team_id:
+ values['team_id'] = self.partner_id.team_id.id
+ self.update(values)
+
+ @api.model
+ def create(self, vals):
+ if vals.get('name', 'New') == 'New':
+ vals['name'] = self.env['ir.sequence'].next_by_code('sale.order') or 'New'
+ if any(f not in vals for f in ['partner_invoice_id', 'partner_shipping_id', 'pricelist_id']):
+ partner = self.env['res.partner'].browse(vals.get('partner_id'))
+ addr = partner.address_get(['delivery', 'invoice'])
+ vals['partner_invoice_id'] = vals.setdefault('partner_invoice_id', addr['invoice'])
+ vals['partner_shipping_id'] = vals.setdefault('partner_shipping_id', addr['delivery'])
+ vals['pricelist_id'] = vals.setdefault('pricelist_id', partner.property_product_pricelist and partner.property_product_pricelist.id)
+ result = super(SaleOrder, self).create(vals)
+ self.message_post(body=_("Quotation created"))
+ return result
+
+ @api.multi
+ def _prepare_invoice(self):
"""Prepare the dict of values to create the new invoice for a
sales order. This method may be overridden to implement custom
invoice generation (making sure to call super() to establish
a clean extension chain).
-
- :param browse_record order: sale.order record to invoice
- :param list(int) line: list of invoice line IDs that must be
- attached to the invoice
- :return: dict of value to create() the invoice
"""
- if context is None:
- context = {}
- journal_ids = self.pool.get('account.journal').search(cr, uid,
- [('type', '=', 'sale'), ('company_id', '=', order.company_id.id)],
- limit=1)
+ self.ensure_one()
+ journal_ids = self.env['account.journal'].search([('type', '=', 'sale'), ('company_id', '=', self.company_id.id)], limit=1)
if not journal_ids:
- raise UserError(_('Please define sales journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id))
+ raise UserError(_('Please define an accounting sale journal for this company.'))
invoice_vals = {
- 'name': order.client_order_ref or '',
- 'origin': order.name,
+ 'name': self.client_order_ref or '',
+ 'origin': self.name,
'type': 'out_invoice',
- 'reference': order.client_order_ref or order.name,
- 'account_id': order.partner_invoice_id.property_account_receivable_id.id,
- 'partner_id': order.partner_invoice_id.id,
- 'journal_id': journal_ids[0],
- 'invoice_line_ids': [(6, 0, lines)],
- 'currency_id': order.pricelist_id.currency_id.id,
- 'comment': order.note,
- 'payment_term_id': order.payment_term_id.id,
- 'fiscal_position_id': order.fiscal_position_id.id or order.partner_invoice_id.property_account_position_id.id,
- 'date_invoice': context.get('date_invoice', False),
- 'company_id': order.company_id.id,
- 'user_id': order.user_id and order.user_id.id or False,
- 'team_id' : order.team_id.id
+ 'reference': self.client_order_ref or self.name,
+ 'account_id': self.partner_invoice_id.property_account_receivable_id.id,
+ 'partner_id': self.partner_invoice_id.id,
+ 'journal_id': journal_ids[0].id,
+ 'currency_id': self.pricelist_id.currency_id.id,
+ 'comment': self.note,
+ 'payment_term_id': self.payment_term_id.id,
+ 'fiscal_position_id': self.fiscal_position_id.id or self.partner_invoice_id.property_account_position_id.id,
+ 'company_id': self.company_id.id,
+ 'user_id': self.user_id and self.user_id.id,
+ 'team_id': self.team_id.id
}
-
- # Care for deprecated _inv_get() hook - FIXME: to be removed after 6.1
- invoice_vals.update(self._inv_get(cr, uid, order, context=context))
return invoice_vals
- def _make_invoice(self, cr, uid, order, lines, context=None):
- inv_obj = self.pool.get('account.invoice')
- obj_invoice_line = self.pool.get('account.invoice.line')
- if context is None:
- context = {}
- invoiced_sale_line_ids = self.pool.get('sale.order.line').search(cr, uid, [('order_id', '=', order.id), ('invoiced', '=', True)], context=context)
- from_line_invoice_ids = []
- for invoiced_sale_line_id in self.pool.get('sale.order.line').browse(cr, uid, invoiced_sale_line_ids, context=context):
- for invoice_line_id in invoiced_sale_line_id.invoice_lines:
- if invoice_line_id.invoice_id.id not in from_line_invoice_ids:
- from_line_invoice_ids.append(invoice_line_id.invoice_id.id)
- for preinv in order.invoice_ids:
- if preinv.state not in ('cancel',) and preinv.id not in from_line_invoice_ids:
- for preline in preinv.invoice_line_ids:
- inv_line_id = obj_invoice_line.copy(cr, uid, preline.id, {'invoice_id': False, 'price_unit': -preline.price_unit})
- lines.append(inv_line_id)
- inv = self._prepare_invoice(cr, uid, order, lines, context=context)
- inv_id = inv_obj.create(cr, uid, inv, context=context)
- invoice = self.pool['account.invoice'].browse(cr, uid, inv_id, context=context)
- invoice._onchange_payment_term_date_invoice()
- invoice.compute_taxes()
- return inv_id
+ @api.multi
+ def print_quotation(self):
+ self.filtered(lambda s: s.state == 'draft').write({'state': 'sent'})
+ return self.env['report'].get_action(self, 'sale.report_saleorder')
- def print_quotation(self, cr, uid, ids, context=None):
- '''
- This function prints the sales order and mark it as sent, so that we can see more easily the next step of the workflow
- '''
- assert len(ids) == 1, 'This option should only be used for a single id at a time'
- self.signal_workflow(cr, uid, ids, 'quotation_sent')
- return self.pool['report'].get_action(cr, uid, ids, 'sale.report_saleorder', context=context)
+ @api.multi
+ def action_view_invoice(self):
+ self.ensure_one()
+ imd = self.env['ir.model.data']
+ action = imd.xmlid_to_object('account.action_invoice_tree1')
+ list_view_id = imd.xmlid_to_res_id('account.invoice_tree')
+ form_view_id = imd.xmlid_to_res_id('account.invoice_form')
- def manual_invoice(self, cr, uid, ids, context=None):
- """ create invoices for the given sales orders (ids), and open the form
- view of one of the newly created invoices
- """
- mod_obj = self.pool.get('ir.model.data')
-
- # create invoices through the sales orders' workflow
- inv_ids0 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
- self.signal_workflow(cr, uid, ids, 'manual_invoice')
- inv_ids1 = set(inv.id for sale in self.browse(cr, uid, ids, context) for inv in sale.invoice_ids)
- # determine newly created invoices
- new_inv_ids = list(inv_ids1 - inv_ids0)
-
- res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
- res_id = res and res[1] or False,
-
- return {
- 'name': _('Customer Invoices'),
- 'view_type': 'form',
- 'view_mode': 'form',
- 'view_id': [res_id],
- 'res_model': 'account.invoice',
- 'context': "{'type':'out_invoice'}",
- 'type': 'ir.actions.act_window',
- 'target': 'current',
- 'res_id': new_inv_ids and new_inv_ids[0] or False,
+ result = {
+ 'name': action.name,
+ 'help': action.help,
+ 'type': action.type,
+ 'views': [[list_view_id, 'tree'], [form_view_id, 'form'], [False, 'graph'], [False, 'kanban'], [False, 'calendar'], [False, 'pivot']],
+ 'target': action.target,
+ 'context': action.context,
+ 'res_model': action.res_model,
}
-
- def action_view_invoice(self, cr, uid, ids, context=None):
- '''
- This function returns an action that display existing invoices of given sales order ids. It can either be a in a list or in a form view, if there is only one invoice 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, 'account', 'action_invoice_tree1')
- id = result and result[1] or False
- result = act_obj.read(cr, uid, [id], context=context)[0]
- #compute the number of invoices to display
- inv_ids = []
- for so in self.browse(cr, uid, ids, context=context):
- inv_ids += [invoice.id for invoice in so.invoice_ids]
- #choose the view_mode accordingly
- if len(inv_ids)>1:
- result['domain'] = "[('id','in',["+','.join(map(str, inv_ids))+"])]"
+ if len(self.invoice_ids) > 1:
+ result['domain'] = "[('id','in',%s)]" % self.invoice_ids.ids
+ elif len(self.invoice_ids) == 1:
+ result['views'] = [(form_view_id, 'form')]
+ result['res_id'] = self.invoice_ids.id
else:
- res = mod_obj.get_object_reference(cr, uid, 'account', 'invoice_form')
- result['views'] = [(res and res[1] or False, 'form')]
- result['res_id'] = inv_ids and inv_ids[0] or False
+ result = {'type': 'ir.actions.act_window_close'}
return result
- def test_no_product(self, cr, uid, order, context):
- for line in order.order_line:
- if line.state == 'cancel':
- continue
- if line.product_id and (line.product_id.type in ['consu', 'product']):
- return False
- return True
-
- def action_invoice_create(self, cr, uid, ids, grouped=False, states=None, date_invoice = False, context=None):
- if states is None:
- states = ['confirmed', 'done', 'exception']
- res = False
+ @api.multi
+ def action_invoice_create(self, grouped=False, final=False):
+ inv_obj = self.env['account.invoice']
+ precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
invoices = {}
- invoice_ids = []
- invoice = self.pool.get('account.invoice')
- obj_sale_order_line = self.pool.get('sale.order.line')
- partner_currency = {}
- # If date was specified, use it as date invoiced, usefull when invoices are generated this month and put the
- # last day of the last month as invoice date
- if date_invoice:
- context = dict(context or {}, date_invoice=date_invoice)
- for o in self.browse(cr, uid, ids, context=context):
- currency_id = o.pricelist_id.currency_id.id
- if (o.partner_id.id in partner_currency) and (partner_currency[o.partner_id.id] <> currency_id):
- raise UserError(_('You cannot group sales having different currencies for the same partner.'))
- partner_currency[o.partner_id.id] = currency_id
- lines = []
- for line in o.order_line:
- if line.invoiced:
+ for order in self:
+ group_key = order.id if grouped else (order.partner_id.id, order.currency_id.id)
+ for line in order.order_line.sorted(key=lambda l: l.qty_to_invoice < 0):
+ if float_is_zero(line.qty_to_invoice, precision_digits=precision):
continue
- elif (line.state in states):
- lines.append(line.id)
- created_lines = obj_sale_order_line.invoice_line_create(cr, uid, lines)
- if created_lines:
- invoices.setdefault(o.partner_invoice_id.id or o.partner_id.id, []).append((o, created_lines))
- if not invoices:
- for o in self.browse(cr, uid, ids, context=context):
- for i in o.invoice_ids:
- if i.state == 'draft':
- return i.id
- for val in invoices.values():
- if grouped:
- res = self._make_invoice(cr, uid, val[0][0], reduce(lambda x, y: x + y, [l for o, l in val], []), context=context)
- invoice_ref = ''
- origin_ref = ''
- for o, l in val:
- invoice_ref += (o.client_order_ref or o.name) + '|'
- origin_ref += (o.origin or o.name) + '|'
- self.write(cr, uid, [o.id], {'state': 'progress'})
- cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (o.id, res))
- self.invalidate_cache(cr, uid, ['invoice_ids'], [o.id], context=context)
- #remove last '|' in invoice_ref
- if len(invoice_ref) >= 1:
- invoice_ref = invoice_ref[:-1]
- if len(origin_ref) >= 1:
- origin_ref = origin_ref[:-1]
- invoice.write(cr, uid, [res], {'origin': origin_ref, 'name': invoice_ref})
- else:
- for order, il in val:
- res = self._make_invoice(cr, uid, order, il, context=context)
- invoice_ids.append(res)
- self.write(cr, uid, [order.id], {'state': 'progress'})
- cr.execute('insert into sale_order_invoice_rel (order_id,invoice_id) values (%s,%s)', (order.id, res))
- self.invalidate_cache(cr, uid, ['invoice_ids'], [order.id], context=context)
- return res
+ if group_key not in invoices:
+ inv_data = order._prepare_invoice()
+ invoice = inv_obj.create(inv_data)
+ invoices[group_key] = invoice
+ if line.qty_to_invoice > 0:
+ line.invoice_line_create(invoices[group_key].id, line.qty_to_invoice)
+ elif line.qty_to_invoice < 0 and (final or invoices[group_key].amount_untaxed > abs(line.qty_to_invoice * line.price_unit)):
+ line.invoice_line_create(invoices[group_key].id, line.qty_to_invoice)
- def action_invoice_cancel(self, cr, uid, ids, context=None):
- self.write(cr, uid, ids, {'state': 'invoice_except'}, context=context)
- return True
+ for invoice in invoices.values():
+ # If invoice is negative, do a refund invoice instead
+ if invoice.amount_untaxed < 0:
+ invoice.type = 'out_refund'
+ for line in invoice.invoice_line_ids:
+ line.quantity = -line.quantity
+ # Necessary to force computation of taxes. In account_invoice, they are triggered
+ # by onchanges, which are not triggered when doing a create.
+ invoice.compute_taxes()
- def action_invoice_end(self, cr, uid, ids, context=None):
- for this in self.browse(cr, uid, ids, context=context):
- for line in this.order_line:
- if line.state == 'exception':
- line.write({'state': 'confirmed'})
- if this.state == 'invoice_except':
- this.write({'state': 'progress'})
- return True
+ return [inv.id for inv in invoices.values()]
- def action_cancel(self, cr, uid, ids, context=None):
- if context is None:
- context = {}
- sale_order_line_obj = self.pool.get('sale.order.line')
- account_invoice_obj = self.pool.get('account.invoice')
- for sale in self.browse(cr, uid, ids, context=context):
- for inv in sale.invoice_ids:
- if inv.state not in ('draft', 'cancel'):
- raise UserError(_('Cannot cancel this sales order!') + ':' + _('First cancel all invoices attached to this sales order.'))
- inv.signal_workflow('invoice_cancel')
- line_ids = [l.id for l in sale.order_line if l.state != 'cancel']
- sale_order_line_obj.button_cancel(cr, uid, line_ids, context=context)
- self.write(cr, uid, ids, {'state': 'cancel'})
- return True
+ @api.multi
+ def action_draft(self):
+ self.filtered(lambda s: s.state in ['cancel', 'sent']).write({'state': 'draft'})
- def action_button_confirm(self, cr, uid, ids, context=None):
- if not context:
- context = {}
- assert len(ids) == 1, 'This option should only be used for a single id at a time.'
- self.signal_workflow(cr, uid, ids, 'order_confirm')
- if context.get('send_email'):
- self.force_quotation_send(cr, uid, ids, context=context)
- return True
+ @api.multi
+ def action_cancel(self):
+ self.write({'state': 'cancel'})
- def action_wait(self, cr, uid, ids, context=None):
- context = context or {}
- for o in self.browse(cr, uid, ids):
- if not any(line.state != 'cancel' for line in o.order_line):
- raise UserError(_('You cannot confirm a sales order which has no line.'))
- noprod = self.test_no_product(cr, uid, o, context)
- if (o.order_policy == 'manual') or noprod:
- self.write(cr, uid, [o.id], {'state': 'manual', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
- else:
- self.write(cr, uid, [o.id], {'state': 'progress', 'date_confirm': fields.date.context_today(self, cr, uid, context=context)})
- self.pool.get('sale.order.line').button_confirm(cr, uid, [x.id for x in o.order_line if x.state != 'cancel'])
- return True
-
- def action_quotation_send(self, cr, uid, ids, context=None):
+ @api.multi
+ def action_quotation_send(self):
'''
This function opens a window to compose an email, with the edi sale template message loaded by default
'''
- assert len(ids) == 1, 'This option should only be used for a single id at a time.'
- ir_model_data = self.pool.get('ir.model.data')
+ self.ensure_one()
+ ir_model_data = self.env['ir.model.data']
try:
- template_id = ir_model_data.get_object_reference(cr, uid, 'sale', 'email_template_edi_sale')[1]
+ template_id = ir_model_data.get_object_reference('sale', 'email_template_edi_sale')[1]
except ValueError:
template_id = False
try:
- compose_form_id = ir_model_data.get_object_reference(cr, uid, 'mail', 'email_compose_message_wizard_form')[1]
+ compose_form_id = ir_model_data.get_object_reference('mail', 'email_compose_message_wizard_form')[1]
except ValueError:
compose_form_id = False
ctx = dict()
ctx.update({
'default_model': 'sale.order',
- 'default_res_id': ids[0],
+ 'default_res_id': self.ids[0],
'default_use_template': bool(template_id),
'default_template_id': template_id,
'default_composition_mode': 'comment',
@@ -573,12 +319,12 @@ class sale_order(osv.osv):
'context': ctx,
}
- def force_quotation_send(self, cr, uid, ids, context=None):
- for order_id in ids:
- email_act = self.action_quotation_send(cr, uid, [order_id], context=context)
+ @api.multi
+ def force_quotation_send(self):
+ for order in self:
+ email_act = order.action_quotation_send()
if email_act and email_act.get('context'):
- composer_obj = self.pool['mail.compose.message']
- composer_values = {}
+ composer_obj = self.env['mail.compose.message']
email_ctx = email_act['context']
template_values = [
email_ctx.get('default_template_id'),
@@ -586,672 +332,442 @@ class sale_order(osv.osv):
email_ctx.get('default_model'),
email_ctx.get('default_res_id'),
]
- composer_values.update(composer_obj.onchange_template_id(cr, uid, None, *template_values, context=context).get('value', {}))
+ composer_values = composer_obj.onchange_template_id(*template_values).get('value', {})
if not composer_values.get('email_from'):
- composer_values['email_from'] = self.browse(cr, uid, order_id, context=context).company_id.email
+ composer_values['email_from'] = order.company_id.email
for key in ['attachment_ids', 'partner_ids']:
if composer_values.get(key):
composer_values[key] = [(6, 0, composer_values[key])]
- composer_id = composer_obj.create(cr, uid, composer_values, context=email_ctx)
- composer_obj.send_mail(cr, uid, [composer_id], context=email_ctx)
+ composer_id = composer_obj.with_context(email_ctx).create(composer_values)
+ composer_id.with_context(email_ctx).send_mail()
return True
- def action_done(self, cr, uid, ids, context=None):
- for order in self.browse(cr, uid, ids, context=context):
- self.pool.get('sale.order.line').write(cr, uid, [line.id for line in order.order_line if line.state != 'cancel'], {'state': 'done'}, context=context)
- return self.write(cr, uid, ids, {'state': 'done'}, context=context)
+ @api.multi
+ def action_done(self):
+ self.write({'state': 'done'})
- def _prepare_order_line_procurement(self, cr, uid, order, line, group_id=False, context=None):
- date_planned = self._get_date_planned(cr, uid, order, line, order.date_order, context=context)
- 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,
- 'company_id': order.company_id.id,
- 'group_id': group_id,
- 'sale_line_id': line.id
- }
+ @api.model
+ def _prepare_procurement_group(self):
+ return {'name': self.name}
- def _get_date_planned(self, cr, uid, order, line, start_date, context=None):
- date_planned = datetime.strptime(start_date, DEFAULT_SERVER_DATETIME_FORMAT) + timedelta(days=line.delay or 0.0)
- return date_planned
+ @api.multi
+ def action_confirm(self):
+ for order in self:
+ order.state = 'sale'
+ order.order_line._action_procurement_create()
+ if not order.project_id:
+ for line in order.order_line:
+ if line.product_id.invoice_policy == 'cost':
+ order._create_analytic_account()
+ break
- def _prepare_procurement_group(self, cr, uid, order, context=None):
- return {'name': order.name, 'partner_id': order.partner_shipping_id.id}
-
- def procurement_needed(self, cr, uid, ids, context=None):
- #when sale is installed only, there is no need to create procurements, that's only
- #further installed modules (sale_service, sale_stock) that will change this.
- sale_line_obj = self.pool.get('sale.order.line')
- res = []
- for order in self.browse(cr, uid, ids, context=context):
- res.append(sale_line_obj.need_procurement(cr, uid, [line.id for line in order.order_line if line.state != 'cancel'], context=context))
- return any(res)
-
- def action_ignore_delivery_exception(self, cr, uid, ids, context=None):
- for sale_order in self.browse(cr, uid, ids, context=context):
- self.write(cr, uid, ids, {'state': 'progress' if sale_order.invoice_count else 'manual'}, context=context)
- return True
-
- def action_ship_create(self, cr, uid, ids, 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.
-
- :return: True
- """
- context = context or {}
- context['lang'] = self.pool['res.users'].browse(cr, uid, uid).lang
- procurement_obj = self.pool.get('procurement.order')
- sale_line_obj = self.pool.get('sale.order.line')
- for order in self.browse(cr, uid, ids, context=context):
- proc_ids = []
- vals = self._prepare_procurement_group(cr, uid, order, context=context)
- if not order.procurement_group_id:
- group_id = self.pool.get("procurement.group").create(cr, uid, vals, context=context)
- order.write({'procurement_group_id': group_id})
-
- for line in order.order_line:
- if line.state == 'cancel':
- continue
- #Try to fix exception procurement (possible when after a shipping exception the user choose to recreate)
- if line.procurement_ids:
- #first check them to see if they are in exception or not (one of the related moves is cancelled)
- procurement_obj.check(cr, uid, [x.id for x in line.procurement_ids if x.state not in ['cancel', 'done']])
- line.refresh()
- #run again procurement that are in exception in order to trigger another move
- except_proc_ids = [x.id for x in line.procurement_ids if x.state in ('exception', 'cancel')]
- procurement_obj.reset_to_confirmed(cr, uid, except_proc_ids, context=context)
- proc_ids += except_proc_ids
- elif sale_line_obj.need_procurement(cr, uid, [line.id], context=context):
- if (line.state == 'done') or not line.product_id:
- continue
- vals = self._prepare_order_line_procurement(cr, uid, order, line, group_id=order.procurement_group_id.id, context=context)
- ctx = context.copy()
- ctx['procurement_autorun_defer'] = True
- proc_id = procurement_obj.create(cr, uid, vals, context=ctx)
- proc_ids.append(proc_id)
- #Confirm procurement order such that rules will be applied on it
- #note that the workflow normally ensure proc_ids isn't an empty list
- procurement_obj.run(cr, uid, proc_ids, context=context)
-
- #if shipping was in exception and the user choose to recreate the delivery order, write the new status of SO
- if order.state == 'shipping_except':
- val = {'state': 'progress', '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
+ @api.multi
+ def _create_analytic_account(self, prefix=None):
+ for order in self:
+ name = order.name
+ if prefix:
+ name = prefix + ": " + order.name
+ analytic = self.env['account.analytic.account'].create({
+ 'name': name,
+ 'code': order.client_order_ref,
+ 'company_id': order.company_id.id,
+ 'partner_id': order.partner_id.id
+ })
+ order.project_id = analytic
-
- def onchange_fiscal_position(self, cr, uid, ids, fiscal_position_id, order_lines, context=None):
- '''Update taxes of order lines for each line where a product is defined
-
- :param list ids: not used
- :param int fiscal_position_id: sale order fiscal position
- :param list order_lines: command list for one2many write method
- '''
- order_line = []
- fiscal_obj = self.pool.get('account.fiscal.position')
- product_obj = self.pool.get('product.product')
- line_obj = self.pool.get('sale.order.line')
-
- fpos = False
- if fiscal_position_id:
- fpos = fiscal_obj.browse(cr, uid, fiscal_position_id, context=context)
-
- for line in order_lines:
- # create (0, 0, { fields })
- # update (1, ID, { fields })
- if line[0] in [0, 1]:
- prod = None
- if line[2].get('product_id'):
- prod = product_obj.browse(cr, uid, line[2]['product_id'], context=context)
- elif line[1]:
- prod = line_obj.browse(cr, uid, line[1], context=context).product_id
- if prod and prod.taxes_id:
- line[2]['tax_id'] = [[6, 0, fiscal_obj.map_tax(cr, uid, fpos, prod.taxes_id)]]
- order_line.append(line)
-
- # link (4, ID)
- # link all (6, 0, IDS)
- elif line[0] in [4, 6]:
- line_ids = line[0] == 4 and [line[1]] or line[2]
- for line_id in line_ids:
- prod = line_obj.browse(cr, uid, line_id, context=context).product_id
- if prod and prod.taxes_id:
- order_line.append([1, line_id, {'tax_id': [[6, 0, fiscal_obj.map_tax(cr, uid, fpos, prod.taxes_id)]]}])
- else:
- order_line.append([4, line_id])
- else:
- order_line.append(line)
- return {'value': {'order_line': order_line, 'amount_untaxed': False, 'amount_tax': False, 'amount_total': False}}
-
- def test_procurements_done(self, cr, uid, ids, context=None):
- for sale in self.browse(cr, uid, ids, context=context):
- for line in sale.order_line:
- if line.state == 'cancel':
- continue
- if not all([x.state == 'done' for x in line.procurement_ids]):
- return False
- return True
-
- def test_procurements_except(self, cr, uid, ids, context=None):
- for sale in self.browse(cr, uid, ids, context=context):
- for line in sale.order_line:
- if line.state == 'cancel':
- continue
- if any([x.state == 'cancel' for x in line.procurement_ids]):
- return True
- return False
-
-
-# TODO add a field price_unit_uos
-# - update it on change product and unit price
-# - use it in report if there is a uos
-class sale_order_line(osv.osv):
-
- def need_procurement(self, cr, uid, ids, context=None):
- #when sale is installed only, there is no need to create procurements, that's only
- #further installed modules (sale_service, sale_stock) that will change this.
- prod_obj = self.pool.get('product.product')
- for line in self.browse(cr, uid, ids, context=context):
- if prod_obj.need_procurement(cr, uid, [line.product_id.id], context=context):
- return True
- return False
-
- def _amount_line(self, cr, uid, ids, field_name, arg, context=None):
- tax_obj = self.pool.get('account.tax')
- cur_obj = self.pool.get('res.currency')
- res = {}
- if context is None:
- context = {}
- for line in self.browse(cr, uid, ids, context=context):
- price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
- cur_id = line.order_id.pricelist_id.currency_id.id
- if line.tax_id.ids:
- res[line.id] = tax_obj.compute_all(cr, uid, line.tax_id.ids, price, cur_id, line.product_uom_qty, line.product_id.id, line.order_id.partner_id.id)['total_excluded']
- else:
- res[line.id] = price * line.product_uom_qty
- return res
-
- def _get_uom_id(self, cr, uid, *args):
- try:
- proxy = self.pool.get('ir.model.data')
- result = proxy.get_object_reference(cr, uid, 'product', 'product_uom_unit')
- return result[1]
- except Exception, ex:
- return False
-
- def _fnct_line_invoiced(self, cr, uid, ids, field_name, args, context=None):
- res = dict.fromkeys(ids, False)
- for this in self.browse(cr, uid, ids, context=context):
- res[this.id] = this.invoice_lines and \
- all(iline.invoice_id.state != 'cancel' for iline in this.invoice_lines)
- return res
-
- def _order_lines_from_invoice(self, cr, uid, ids, context=None):
- # direct access to the m2m table is the less convoluted way to achieve this (and is ok ACL-wise)
- cr.execute("""SELECT DISTINCT sol.id FROM sale_order_invoice_rel rel JOIN
- sale_order_line sol ON (sol.order_id = rel.order_id)
- WHERE rel.invoice_id = ANY(%s)""", (list(ids),))
- return [i[0] for i in cr.fetchall()]
-
- def _get_price_reduce(self, cr, uid, ids, field_name, arg, context=None):
- res = dict.fromkeys(ids, 0.0)
- for line in self.browse(cr, uid, ids, context=context):
- res[line.id] = line.price_subtotal / line.product_uom_qty
- return res
-
+class SaleOrderLine(models.Model):
_name = 'sale.order.line'
_description = 'Sales Order Line'
- _columns = {
- 'order_id': fields.many2one('sale.order', 'Order Reference', required=True, ondelete='cascade', select=True, readonly=True, states={'draft':[('readonly',False)]}),
- 'name': fields.text('Description', required=True, readonly=True, states={'draft': [('readonly', False)]}),
- 'sequence': fields.integer('Sequence', help="Gives the sequence order when displaying a list of sales order lines."),
- 'product_id': fields.many2one('product.product', 'Product', domain=[('sale_ok', '=', True)], change_default=True, readonly=True, states={'draft': [('readonly', False)]}, ondelete='restrict'),
- 'invoice_lines': fields.many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True, copy=False),
- 'invoiced': fields.function(_fnct_line_invoiced, string='Invoiced', type='boolean',
- store={
- 'account.invoice': (_order_lines_from_invoice, ['state'], 10),
- 'sale.order.line': (lambda self,cr,uid,ids,ctx=None: ids, ['invoice_lines'], 10)
- }),
- 'price_unit': fields.float('Unit Price', required=True, digits_compute= dp.get_precision('Product Price'), readonly=True, states={'draft': [('readonly', False)]}),
- 'price_subtotal': fields.function(_amount_line, string='Subtotal', digits=0),
- 'price_reduce': fields.function(_get_price_reduce, type='float', string='Price Reduce', digits_compute=dp.get_precision('Product Price')),
- 'tax_id': fields.many2many('account.tax', 'sale_order_tax', 'order_line_id', 'tax_id', 'Taxes', readonly=True, states={'draft': [('readonly', False)]}),
- 'product_uom_qty': fields.float('Quantity', digits_compute= dp.get_precision('Product UoS'), required=True, readonly=True, states={'draft': [('readonly', False)]}),
- 'product_uom': fields.many2one('product.uom', 'Unit of Measure ', required=True, readonly=True, states={'draft': [('readonly', False)]}),
- 'product_uos_qty': fields.float('Quantity (UoS)' ,digits_compute= dp.get_precision('Product UoS'), readonly=True, states={'draft': [('readonly', False)]}),
- 'product_uos': fields.many2one('product.uom', 'Product UoS'),
- 'discount': fields.float('Discount (%)', digits_compute= dp.get_precision('Discount'), readonly=True, states={'draft': [('readonly', False)]}),
- 'th_weight': fields.float('Weight', readonly=True, states={'draft': [('readonly', False)]}),
- 'state': fields.selection(
- [('cancel', 'Cancelled'),('draft', 'Draft'),('confirmed', 'Confirmed'),('exception', 'Exception'),('done', 'Done')],
- 'Status', required=True, readonly=True, copy=False,
- help='* The \'Draft\' status is set when the related sales order in draft status. \
- \n* The \'Confirmed\' status is set when the related sales order is confirmed. \
- \n* The \'Exception\' status is set when the related sales order is set as exception. \
- \n* The \'Done\' status is set when the sales order line has been picked. \
- \n* The \'Cancelled\' status is set when a user cancel the sales order related.'),
- 'order_partner_id': fields.related('order_id', 'partner_id', type='many2one', relation='res.partner', store=True, string='Customer'),
- 'salesman_id':fields.related('order_id', 'user_id', type='many2one', relation='res.users', store=True, string='Salesperson'),
- 'company_id': fields.related('order_id', 'company_id', type='many2one', relation='res.company', string='Company', store=True, readonly=True),
- '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_ids': fields.one2many('procurement.order', 'sale_line_id', 'Procurements'),
- }
_order = 'order_id desc, sequence, id'
- _defaults = {
- 'product_uom' : _get_uom_id,
- 'discount': 0.0,
- 'product_uom_qty': 1,
- 'product_uos_qty': 1,
- 'sequence': 10,
- 'state': 'draft',
- 'price_unit': 0.0,
- 'delay': 0.0,
- }
+ @api.depends('state', 'product_uom_qty', 'qty_delivered', 'qty_to_invoice', 'qty_invoiced')
+ def _compute_invoice_status(self):
+ precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
+ for line in self:
+ if line.state not in ('sale', 'done'):
+ line.invoice_status = 'no'
+ elif not float_is_zero(line.qty_to_invoice, precision_digits=precision):
+ line.invoice_status = 'to invoice'
+ elif float_compare(line.qty_invoiced, line.product_uom_qty, precision_digits=precision) == 1 or\
+ float_compare(line.qty_invoiced, line.product_uom_qty, precision_digits=precision) >= 0 and\
+ float_compare(line.qty_delivered, line.product_uom_qty, precision_digits=precision) == 1:
+ line.invoice_status = 'upselling'
+ elif float_compare(line.qty_invoiced, line.product_uom_qty, precision_digits=precision) == 0:
+ line.invoice_status = 'invoiced'
+ else:
+ line.invoice_status = 'no'
+ @api.depends('product_uom_qty', 'discount', 'price_unit', 'tax_id')
+ def _compute_amount(self):
+ for line in self:
+ price = line.price_unit * (1 - (line.discount or 0.0) / 100.0)
+ taxes = line.tax_id.compute_all(price, line.order_id.currency_id, line.product_uom_qty, product=line.product_id, partner=line.order_id.partner_id)
+ line.update({
+ 'price_tax': taxes['total_included'] - taxes['total_excluded'],
+ 'price_total': taxes['total_included'],
+ 'price_subtotal': taxes['total_excluded'],
+ })
- def _get_line_qty(self, cr, uid, line, context=None):
- if line.product_uos:
- return line.product_uos_qty or 0.0
- return line.product_uom_qty
+ @api.depends('product_id.invoice_policy', 'order_id.state')
+ def _compute_qty_delivered_updateable(self):
+ for line in self:
+ line.qty_delivered_updateable = line.product_id.invoice_policy in ('order', 'delivery') and line.order_id.state == 'sale'
- def _get_line_uom(self, cr, uid, line, context=None):
- if line.product_uos:
- return line.product_uos.id
- return line.product_uom.id
+ @api.depends('qty_invoiced', 'qty_delivered', 'product_uom_qty', 'order_id.state')
+ def _get_to_invoice_qty(self):
+ for line in self:
+ if line.order_id.state in ['sale', 'done']:
+ if line.product_id.invoice_policy == 'order':
+ line.qty_to_invoice = line.product_uom_qty - line.qty_invoiced
+ else:
+ line.qty_to_invoice = line.qty_delivered - line.qty_invoiced
+ else:
+ line.qty_to_invoice = 0
- def _prepare_order_line_invoice_line(self, cr, uid, line, account_id=False, context=None):
+ @api.depends('invoice_lines.invoice_id.state', 'invoice_lines.quantity')
+ def _get_invoice_qty(self):
+ for line in self:
+ qty_invoiced = 0.0
+ for invoice_line in line.invoice_lines:
+ if invoice_line.invoice_id.state != 'cancel':
+ if invoice_line.invoice_id.type == 'out_invoice':
+ qty_invoiced += invoice_line.quantity
+ elif invoice_line.invoice_id.type == 'out_refund':
+ qty_invoiced -= invoice_line.quantity
+ line.qty_invoiced = qty_invoiced
+
+ @api.depends('price_subtotal', 'product_uom_qty')
+ def _get_price_reduce(self):
+ for line in self:
+ line.price_reduce = line.price_subtotal / line.product_uom_qty if line.product_uom_qty else 0.0
+
+ @api.onchange('order_id', 'product_id')
+ def _compute_tax_id(self):
+ for line in self:
+ fpos = line.order_id.fiscal_position_id or line.order_id.partner_id.property_account_position_id
+ if fpos:
+ # The superuser is used by website_sale in order to create a sale order. We need to make
+ # sure we only select the taxes related to the company of the partner. This should only
+ # apply if the partner is linked to a company.
+ if self.env.uid == SUPERUSER_ID and line.order_id.company_id:
+ taxes = fpos.map_tax(line.product_id.taxes_id).filtered(lambda r: r.company_id == line.order_id.company_id)
+ else:
+ taxes = fpos.map_tax(line.product_id.taxes_id)
+ line.tax_id = taxes
+ else:
+ line.tax_id = False
+
+ @api.multi
+ def _prepare_order_line_procurement(self, group_id=False):
+ self.ensure_one()
+ return {
+ 'name': self.name,
+ 'origin': self.order_id.name,
+ 'date_planned': datetime.strptime(self.order_id.date_order, DEFAULT_SERVER_DATETIME_FORMAT) + timedelta(days=self.customer_lead),
+ 'product_id': self.product_id.id,
+ 'product_qty': self.product_uom_qty,
+ 'product_uom': self.product_uom.id,
+ 'company_id': self.order_id.company_id.id,
+ 'group_id': group_id,
+ 'sale_line_id': self.id
+ }
+
+ @api.multi
+ def _action_procurement_create(self):
+ precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
+ for line in self:
+ if line.state != 'sale':
+ continue
+ qty = 0.0
+ for proc in line.procurement_ids:
+ qty += proc.product_qty
+ if float_compare(qty, line.product_uom_qty, precision_digits=precision) >= 0:
+ return False
+
+ if not line.order_id.procurement_group_id:
+ vals = line.order_id._prepare_procurement_group()
+ line.order_id.procurement_group_id = self.env["procurement.group"].create(vals)
+
+ vals = line._prepare_order_line_procurement(group_id=line.order_id.procurement_group_id.id)
+ vals['product_qty'] = line.product_uom_qty - qty
+ new_proc = self.env["procurement.order"].create(vals)
+ new_proc.run()
+ return True
+
+ # Create new procurements if quantities purchased changes
+ @api.model
+ def create(self, values):
+ line = super(SaleOrderLine, self).create(values)
+ if line.state == 'sale':
+ line._action_procurement_create()
+ return line
+
+ # Create new procurements if quantities purchased changes
+ @api.multi
+ def write(self, values):
+ precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
+ lines = False
+ if 'product_uom_qty' in values:
+ lines = self.filtered(
+ lambda r: r.state == 'sale' and float_compare(r.product_uom_qty, values['product_uom_qty'], precision_digits=precision) == -1)
+ lines._action_procurement_create()
+ return super(SaleOrderLine, self).write(values)
+
+ order_id = fields.Many2one('sale.order', string='Order Reference', required=True, ondelete='cascade', index=True, copy=False)
+ name = fields.Text(string='Description', required=True)
+ sequence = fields.Integer(string='Sequence', default=10)
+
+ invoice_lines = fields.Many2many('account.invoice.line', 'sale_order_line_invoice_rel', 'order_line_id', 'invoice_line_id', string='Invoice Lines', copy=False)
+ invoice_status = fields.Selection([
+ ('upselling', 'Upselling Opportunity'),
+ ('invoiced', 'Fully Invoiced'),
+ ('to invoice', 'To Invoice'),
+ ('no', 'Nothing to Invoice')
+ ], string='Invoice Status', compute='_compute_invoice_status', store=True, readonly=True, default='no')
+ price_unit = fields.Float('Unit Price', required=True, digits_compute=dp.get_precision('Product Price'), default=0.0)
+
+ price_subtotal = fields.Monetary(compute='_compute_amount', string='Subtotal', readonly=True, store=True)
+ price_tax = fields.Monetary(compute='_compute_amount', string='Taxes', readonly=True, store=True)
+ price_total = fields.Monetary(compute='_compute_amount', string='Total', readonly=True, store=True)
+
+ price_reduce = fields.Monetary(compute='_get_price_reduce', string='Price Reduce', readonly=True, store=True)
+ tax_id = fields.Many2many('account.tax', string='Taxes', readonly=True, states={'draft': [('readonly', False)]})
+
+ discount = fields.Float(string='Discount (%)', digits_compute=dp.get_precision('Discount'), default=0.0)
+
+ product_id = fields.Many2one('product.product', string='Product', domain=[('sale_ok', '=', True)], change_default=True, ondelete='restrict', required=True)
+ product_uom_qty = fields.Float(string='Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True, default=1.0)
+ product_uom = fields.Many2one('product.uom', string='Unit of Measure', required=True)
+
+ qty_delivered_updateable = fields.Boolean(compute='_compute_qty_delivered_updateable', string='Can Edit Delivered', readonly=True, default=True)
+ qty_delivered = fields.Float(string='Delivered', copy=False, digits_compute=dp.get_precision('Product Unit of Measure'), default=0.0)
+ qty_to_invoice = fields.Float(
+ compute='_get_to_invoice_qty', string='To Invoice', store=True, readonly=True,
+ digits_compute=dp.get_precision('Product Unit of Measure'), default=0.0)
+ qty_invoiced = fields.Float(
+ compute='_get_invoice_qty', string='Invoiced', store=True, readonly=True,
+ digits_compute=dp.get_precision('Product Unit of Measure'), default=0.0)
+
+ salesman_id = fields.Many2one(related='order_id.user_id', store=True, string='Salesperson', readonly=True)
+ currency_id = fields.Many2one(related='order_id.currency_id', store=True, string='Currency', readonly=True)
+ company_id = fields.Many2one(related='order_id.company_id', string='Company', store=True, readonly=True)
+ order_partner_id = fields.Many2one(related='order_id.partner_id', store=True, string='Customer')
+
+ state = fields.Selection([
+ ('draft', 'Quotation'),
+ ('sent', 'Quotation Sent'),
+ ('sale', 'Sale Order'),
+ ('done', 'Done'),
+ ('cancel', 'Cancelled'),
+ ], related='order_id.state', string='Order Status', readonly=True, copy=False, store=True, default='draft')
+
+ customer_lead = fields.Float(
+ 'Delivery Lead Time', required=True, default=0.0,
+ help="Number of days between the order confirmation and the shipping of the products to the customer", oldname="delay")
+ procurement_ids = fields.One2many('procurement.order', 'sale_line_id', string='Procurements')
+
+ @api.multi
+ def _prepare_invoice_line(self, qty):
"""Prepare the dict of values to create the new invoice line for a
sales order line. This method may be overridden to implement custom
invoice generation (making sure to call super() to establish
a clean extension chain).
- :param browse_record line: sale.order.line record to invoice
- :param int account_id: optional ID of a G/L account to force
- (this is used for returning products including service)
- :return: dict of values to create() the invoice line
+ :param qty : float quantity to invoice
"""
+ self.ensure_one()
res = {}
- if not line.invoiced:
- if not account_id:
- if line.product_id:
- account_id = line.product_id.property_account_income_id.id
- if not account_id:
- account_id = line.product_id.categ_id.property_account_income_categ_id.id
- if not account_id:
- raise UserError(
- _('Please define income account for this product: "%s" (id:%d).') % \
- (line.product_id.name, line.product_id.id,))
- else:
- prop = self.pool.get('ir.property').get(cr, uid,
- 'property_account_income_categ_id', 'product.category',
- context=context)
- account_id = prop and prop.id or False
- uosqty = self._get_line_qty(cr, uid, line, context=context)
- uos_id = self._get_line_uom(cr, uid, line, context=context)
- pu = 0.0
- if uosqty:
- pu = round(line.price_unit * line.product_uom_qty / uosqty,
- self.pool.get('decimal.precision').precision_get(cr, uid, 'Product Price'))
- fpos = line.order_id.fiscal_position_id or False
- account_id = self.pool.get('account.fiscal.position').map_account(cr, uid, fpos, account_id)
- if not account_id:
- raise UserError(_('There is no Fiscal Position defined or Income category account defined for default properties of Product categories.'))
- res = {
- 'name': line.name,
- 'sequence': line.sequence,
- 'origin': line.order_id.name,
- 'account_id': account_id,
- 'price_unit': pu,
- 'quantity': uosqty,
- 'discount': line.discount,
- 'uos_id': uos_id,
- 'product_id': line.product_id.id or False,
- 'invoice_line_tax_ids': [(6, 0, [x.id for x in line.tax_id])],
- 'account_analytic_id': line.order_id.project_id and line.order_id.project_id.id or False,
- }
+ account_id = self.product_id.property_account_income_id.id or self.product_id.categ_id.property_account_income_categ_id.id
+ if not account_id:
+ raise UserError(_('Please define income account for this product: "%s" (id:%d) - or for its category: "%s".') % \
+ (self.product_id.name, self.product_id.id, self.product_id.categ_id.name))
- return res
+ fpos = self.order_id.fiscal_position_id or self.order_id.partner_id.property_account_position_id
+ if fpos:
+ account_id = self.order_id.fiscal_position_id.map_account(account_id)
- def invoice_line_create(self, cr, uid, ids, context=None):
- if context is None:
- context = {}
-
- create_ids = []
- sales = set()
- for line in self.browse(cr, uid, ids, context=context):
- vals = self._prepare_order_line_invoice_line(cr, uid, line, False, context)
- if vals:
- inv_id = self.pool.get('account.invoice.line').create(cr, uid, vals, context=context)
- self.write(cr, uid, [line.id], {'invoice_lines': [(4, inv_id)]}, context=context)
- sales.add(line.order_id.id)
- create_ids.append(inv_id)
- # Trigger workflow events
- for sale_id in sales:
- workflow.trg_write(uid, 'sale.order', sale_id, cr)
- return create_ids
-
- def button_cancel(self, cr, uid, ids, context=None):
- lines = self.browse(cr, uid, ids, context=context)
- for line in lines:
- if line.invoiced:
- raise UserError(_('You cannot cancel a sales order line that has already been invoiced.'))
- procurement_obj = self.pool['procurement.order']
- procurement_obj.cancel(cr, uid, sum([l.procurement_ids.ids for l in lines], []), context=context)
- return self.write(cr, uid, ids, {'state': 'cancel'})
-
- def button_confirm(self, cr, uid, ids, context=None):
- return self.write(cr, uid, ids, {'state': 'confirmed'})
-
- def button_done(self, cr, uid, ids, context=None):
- res = self.write(cr, uid, ids, {'state': 'done'})
- for line in self.browse(cr, uid, ids, context=context):
- workflow.trg_write(uid, 'sale.order', line.order_id.id, cr)
- return res
-
- def uos_change(self, cr, uid, ids, product_uos, product_uos_qty=0, product_id=None):
- product_obj = self.pool.get('product.product')
- if not product_id:
- return {'value': {'product_uom': product_uos,
- 'product_uom_qty': product_uos_qty}, 'domain': {}}
-
- product = product_obj.browse(cr, uid, product_id)
- value = {
- 'product_uom': product.uom_id.id,
+ res = {
+ 'name': self.name,
+ 'sequence': self.sequence,
+ 'origin': self.order_id.name,
+ 'account_id': account_id,
+ 'price_unit': self.price_unit,
+ 'quantity': qty,
+ 'discount': self.discount,
+ 'uom_id': self.product_uom.id,
+ 'product_id': self.product_id.id or False,
+ 'invoice_line_tax_ids': [(6, 0, self.tax_id.ids)],
+ 'account_analytic_id': self.order_id.project_id.id,
}
- # FIXME must depend on uos/uom of the product and not only of the coeff.
- try:
- value.update({
- 'product_uom_qty': product_uos_qty / product.uos_coeff,
- 'th_weight': product_uos_qty / product.uos_coeff * product.weight
- })
- except ZeroDivisionError:
- pass
- return {'value': value}
+ return res
- def create(self, cr, uid, values, context=None):
- if values.get('order_id') and values.get('product_id') and any(f not in values for f in ['name', 'price_unit', 'type', 'product_uom_qty', 'product_uom']):
- order = self.pool['sale.order'].read(cr, uid, values['order_id'], ['pricelist_id', 'partner_id', 'date_order', 'fiscal_position_id'], context=context)
- defaults = self.product_id_change(cr, uid, [], order['pricelist_id'][0], values['product_id'],
- qty=float(values.get('product_uom_qty', False)),
- uom=values.get('product_uom', False),
- qty_uos=float(values.get('product_uos_qty', False)),
- uos=values.get('product_uos', False),
- name=values.get('name', False),
- partner_id=order['partner_id'][0],
- date_order=order['date_order'],
- fiscal_position_id=order['fiscal_position_id'][0] if order['fiscal_position_id'] else False,
- flag=False, # Force name update
- context=context
- )['value']
- if defaults.get('tax_id'):
- defaults['tax_id'] = [[6, 0, defaults['tax_id']]]
- values = dict(defaults, **values)
- return super(sale_order_line, self).create(cr, uid, values, context=context)
+ @api.multi
+ def invoice_line_create(self, invoice_id, qty):
+ precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
+ for line in self:
+ if not float_is_zero(qty, precision_digits=precision):
+ vals = line._prepare_invoice_line(qty=qty)
+ vals.update({'invoice_id': invoice_id, 'sale_line_ids': [(6, 0, [line.id])]})
+ self.env['account.invoice.line'].create(vals)
- 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_id=False, flag=False, context=None):
- if context is None:
- context = {}
- Partner = self.pool['res.partner']
- ProductUom = self.pool['product.uom']
- Product = self.pool['product.product']
- ctx_product = dict(context)
- partner = False
- if partner_id:
- partner = Partner.browse(cr, uid, partner_id, context=context)
- ctx_product['lang'] = partner.lang
- ctx_product['partner_id'] = partner_id
- elif lang:
- ctx_product['lang'] = lang
+ @api.multi
+ @api.onchange('product_id')
+ def product_id_change(self):
+ if not self.product_id:
+ return {'domain': {'product_uom': []}}
- if not product:
- return {'value': {'th_weight': 0,
- 'product_uos_qty': qty}, 'domain': {'product_uom': [],
- 'product_uos': []}}
- if not date_order:
- date_order = time.strftime(DEFAULT_SERVER_DATE_FORMAT)
+ vals = {}
+ domain = {'product_uom': [('category_id', '=', self.product_id.uom_id.category_id.id)]}
+ if not (self.product_uom and (self.product_id.uom_id.category_id.id == self.product_uom.category_id.id)):
+ vals['product_uom'] = self.product_id.uom_id
- result = {}
- product_obj = Product.browse(cr, uid, product, context=ctx_product)
+ product = self.product_id.with_context(
+ lang=self.order_id.partner_id.lang,
+ partner=self.order_id.partner_id.id,
+ quantity=self.product_uom_qty,
+ date=self.order_id.date_order,
+ pricelist=self.order_id.pricelist_id.id,
+ uom=self.product_uom.id
+ )
- uom2 = False
- if uom:
- uom2 = ProductUom.browse(cr, uid, uom, context=context)
- if product_obj.uom_id.category_id.id != uom2.category_id.id:
- uom = False
- if uos:
- if product_obj.uos_id:
- uos2 = ProductUom.browse(cr, uid, uos, context=context)
- if product_obj.uos_id.category_id.id != uos2.category_id.id:
- uos = False
- else:
- uos = False
+ name = product.name_get()[0][1]
+ if product.description_sale:
+ name += '\n' + product.description_sale
+ vals['name'] = name
- fpos = False
- if not fiscal_position_id:
- fpos = partner and partner.property_account_position_id or False
- else:
- fpos = self.pool['account.fiscal.position'].browse(cr, uid, fiscal_position_id)
- if update_tax: # The quantity only have changed
- # The superuser is used by website_sale in order to create a sale order. We need to make
- # sure we only select the taxes related to the company of the partner. This should only
- # apply if the partner is linked to a company.
- if uid == SUPERUSER_ID and partner.company_id:
- taxes = product_obj.taxes_id.filtered(lambda r: r.company_id == partner.company_id)
- else:
- taxes = product_obj.taxes_id
- result['tax_id'] = self.pool['account.fiscal.position'].map_tax(cr, uid, fpos, taxes)
+ if self.order_id.pricelist_id and self.order_id.partner_id:
+ vals['price_unit'] = product.price
+ self.update(vals)
+ return {'domain': domain}
- if not flag:
- result['name'] = Product.name_get(cr, uid, [product_obj.id], context=ctx_product)[0][1]
- if product_obj.description_sale:
- result['name'] += '\n'+product_obj.description_sale
- domain = {}
- if (not uom) and (not uos):
- result['product_uom'] = product_obj.uom_id.id
- if product_obj.uos_id:
- result['product_uos'] = product_obj.uos_id.id
- result['product_uos_qty'] = qty * product_obj.uos_coeff
- uos_category_id = product_obj.uos_id.category_id.id
- else:
- result['product_uos'] = False
- result['product_uos_qty'] = qty
- uos_category_id = False
- result['th_weight'] = qty * product_obj.weight
- domain = {'product_uom':
- [('category_id', '=', product_obj.uom_id.category_id.id)],
- 'product_uos':
- [('category_id', '=', uos_category_id)]}
- elif uos and not uom: # only happens if uom is False
- result['product_uom'] = product_obj.uom_id and product_obj.uom_id.id
- result['product_uom_qty'] = qty_uos / product_obj.uos_coeff
- result['th_weight'] = result['product_uom_qty'] * product_obj.weight
- elif uom: # whether uos is set or not
- default_uom = product_obj.uom_id and product_obj.uom_id.id
- q = ProductUom._compute_qty(cr, uid, uom, qty, default_uom)
- if product_obj.uos_id:
- result['product_uos'] = product_obj.uos_id.id
- result['product_uos_qty'] = qty * product_obj.uos_coeff
- else:
- result['product_uos'] = False
- result['product_uos_qty'] = qty
- result['th_weight'] = q * product_obj.weight # Round the quantity up
-
- if not uom2:
- uom2 = product_obj.uom_id
-
- if pricelist and partner_id:
- ctx = dict(
- context,
- uom=uom or result.get('product_uom'),
- date=date_order,
+ @api.onchange('product_uom')
+ def product_uom_change(self):
+ if not self.product_uom:
+ self.price_unit = 0.0
+ return
+ if self.order_id.pricelist_id and self.order_id.partner_id:
+ product = self.product_id.with_context(
+ lang=self.order_id.partner_id.lang,
+ partner=self.order_id.partner_id.id,
+ quantity=self.product_uom_qty,
+ date_order=self.order_id.date_order,
+ pricelist=self.order_id.pricelist_id.id,
+ uom=self.product_uom.id
)
- price = self.pool['product.pricelist'].price_get(cr, uid, [pricelist],
- product, qty or 1.0, partner_id, ctx)[pricelist]
- else:
- price = Product.price_get(cr, uid, [product], ptype='list_price', context=ctx_product)[product] or False
- if context.get('uom_qty_change', False):
- result, domain = {}, {}
- result.update({'price_unit': price})
- return {'value': result, 'domain': domain}
+ self.price_unit = product.price
- def product_uom_change(self, cursor, user, ids, pricelist, product, qty=0,
- uom=False, qty_uos=0, uos=False, name='', partner_id=False,
- lang=False, update_tax=True, date_order=False, context=None):
- context = context or {}
- lang = lang or ('lang' in context and context['lang'])
- if not uom:
- return {'value': {'price_unit': 0.0, 'product_uom' : uom or False}}
- return self.product_id_change(cursor, user, 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, context=context)
+ @api.multi
+ def unlink(self):
+ if self.filtered(lambda x: x.state in ('sale', 'done')):
+ raise UserError(_('You can not remove a sale order line.\nDiscard changes and try setting the quantity to 0.'))
+ return super(SaleOrderLine, self).unlink()
- def unlink(self, cr, uid, ids, context=None):
- if context is None:
- context = {}
- """Allows to delete sales order lines in draft,cancel states"""
- for rec in self.browse(cr, uid, ids, context=context):
- if rec.state not in ['draft', 'cancel']:
- raise UserError(_('Cannot delete a sales order line which is in state \'%s\'.') % (rec.state,))
- return super(sale_order_line, self).unlink(cr, uid, ids, context=context)
+ @api.multi
+ def _get_delivered_qty(self):
+ '''
+ Intended to be overridden in sale_stock and sale_mrp
+ :return: the quantity delivered
+ :rtype: float
+ '''
+ return 0.0
-class mail_compose_message(osv.Model):
+class MailComposeMessage(models.TransientModel):
_inherit = 'mail.compose.message'
- def send_mail(self, cr, uid, ids, auto_commit=False, context=None):
- context = context or {}
- if context.get('default_model') == 'sale.order' and context.get('default_res_id') and context.get('mark_so_as_sent'):
- context = dict(context, mail_post_autofollow=True)
- self.pool.get('sale.order').signal_workflow(cr, uid, [context['default_res_id']], 'quotation_sent')
- return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
+ @api.multi
+ def send_mail(self, auto_commit=False):
+ if self._context.get('default_model') == 'sale.order' and self._context.get('default_res_id') and self._context.get('mark_so_as_sent'):
+ order = self.env['sale.order'].browse([self._context['default_res_id']])
+ if order.state == 'draft':
+ order.state = 'sent'
+ return super(MailComposeMessage, self.with_context(mail_post_autofollow=True)).send_mail()
-class account_invoice(osv.Model):
+class AccountInvoice(models.Model):
_inherit = 'account.invoice'
- _columns = {
- 'team_id': fields.many2one('crm.team', 'Sales Team', oldname='section_id'),
- 'sale_ids': fields.many2many('sale.order', 'sale_order_invoice_rel', 'invoice_id', 'order_id', 'Sale Orders',
- readonly=True, copy=False, help="This is the list of sale orders related to this invoice. One invoice may have multiple sale orders related. "),
- }
+ @api.model
+ def _get_default_team(self):
+ default_team_id = self.env['crm.team']._get_default_team_id()
+ return self.env['crm.team'].browse(default_team_id)
- _defaults = {
- 'team_id': lambda s, cr, uid, c: s.pool['crm.team']._get_default_team_id(cr, uid, context=c),
- }
+ team_id = fields.Many2one('crm.team', string='Sales Team', default=_get_default_team)
- def confirm_paid(self, cr, uid, ids, context=None):
- sale_order_obj = self.pool.get('sale.order')
- res = super(account_invoice, self).confirm_paid(cr, uid, ids, context=context)
- so_ids = sale_order_obj.search(cr, uid, [('invoice_ids', 'in', ids)], context=context)
- for so_id in so_ids:
- sale_order_obj.message_post(cr, uid, so_id, body=_("Invoice paid"), context=context)
+ @api.multi
+ def confirm_paid(self):
+ res = super(AccountInvoice, self).confirm_paid()
+ todo = set()
+ for invoice in self:
+ for line in invoice.invoice_line_ids:
+ for sale_line in line.sale_line_ids:
+ todo.add((sale_line.order_id, invoice.number))
+ for (order, name) in todo:
+ order.message_post(body=_("Invoice %s paid") % (name))
return res
- def unlink(self, cr, uid, ids, context=None):
- """ Overwrite unlink method of account invoice to send a trigger to the sale workflow upon invoice deletion """
- invoice_ids = self.search(cr, uid, [('id', 'in', ids), ('state', 'in', ['draft', 'cancel'])], context=context)
- #if we can't cancel all invoices, do nothing
- if len(invoice_ids) == len(ids):
- #Cancel invoice(s) first before deleting them so that if any sale order is associated with them
- #it will trigger the workflow to put the sale order in an 'invoice exception' state
- for id in ids:
- workflow.trg_validate(uid, 'account.invoice', id, 'invoice_cancel', cr)
- return super(account_invoice, self).unlink(cr, uid, ids, context=context)
-
-class account_invoice_line(osv.Model):
+class AccountInvoiceLine(models.Model):
_inherit = 'account.invoice.line'
-
- _columns= {
- 'sale_line_ids': fields.many2many('sale.order.line', 'sale_order_line_invoice_rel', 'invoice_id', 'order_line_id',
- 'Sale Order Lines', readonly=True, copy=False)
- }
+ sale_line_ids = fields.Many2many('sale.order.line', 'sale_order_line_invoice_rel', 'invoice_line_id', 'order_line_id', string='Sale Order Lines', readonly=True, copy=False)
-class procurement_order(osv.osv):
+class ProcurementOrder(models.Model):
_inherit = 'procurement.order'
- _columns = {
- 'sale_line_id': fields.many2one('sale.order.line', string='Sale Order Line'),
- }
+ sale_line_id = fields.Many2one('sale.order.line', string='Sale Order Line')
- def write(self, cr, uid, ids, vals, context=None):
- if isinstance(ids, (int, long)):
- ids = [ids]
- res = super(procurement_order, self).write(cr, uid, ids, vals, context=context)
- from openerp import workflow
- if vals.get('state') in ['done', 'cancel', 'exception']:
- for proc in self.browse(cr, uid, ids, context=context):
- if proc.sale_line_id and proc.sale_line_id.order_id:
- order_id = proc.sale_line_id.order_id.id
- if self.pool.get('sale.order').test_procurements_done(cr, uid, [order_id], context=context):
- workflow.trg_validate(uid, 'sale.order', order_id, 'ship_end', cr)
- if self.pool.get('sale.order').test_procurements_except(cr, uid, [order_id], context=context):
- workflow.trg_validate(uid, 'sale.order', order_id, 'ship_except', cr)
- return res
-class product_product(osv.Model):
+class ProductProduct(models.Model):
_inherit = 'product.product'
- def _sales_count(self, cr, uid, ids, field_name, arg, context=None):
- r = dict.fromkeys(ids, 0)
+ @api.multi
+ def _sales_count(self):
+ r = {}
domain = [
- ('state', 'in', ['confirmed', 'done']),
- ('product_id', 'in', ids),
+ ('state', 'in', ['sale', 'done']),
+ ('product_id', 'in', self.ids),
]
- for group in self.pool['sale.report'].read_group(cr, uid, domain, ['product_id', 'product_uom_qty'], ['product_id'], context=context):
+ for group in self.env['sale.report'].read_group(domain, ['product_id', 'product_uom_qty'], ['product_id']):
r[group['product_id'][0]] = group['product_uom_qty']
+ for product in self:
+ product.sales_count = r.get(product.id, 0)
return r
- def action_view_sales(self, cr, uid, ids, context=None):
- result = self.pool['ir.model.data'].xmlid_to_res_id(cr, uid, 'sale.action_order_line_product_tree', raise_if_not_found=True)
- result = self.pool['ir.actions.act_window'].read(cr, uid, [result], context=context)[0]
- result['domain'] = "[('product_id','in',[" + ','.join(map(str, ids)) + "])]"
- return result
+ sales_count = fields.Integer(compute='_sales_count', string='# Sales')
- _columns = {
- 'sales_count': fields.function(_sales_count, string='# Sales', type='integer'),
- }
-class product_template(osv.Model):
+class ProductTemplate(models.Model):
_inherit = 'product.template'
+ track_service = fields.Selection([('manual', 'Manually set quantities on order')], string='Track Service', default='manual')
- def _sales_count(self, cr, uid, ids, field_name, arg, context=None):
- res = dict.fromkeys(ids, 0)
- for template in self.browse(cr, uid, ids, context=context):
- res[template.id] = sum([p.sales_count for p in template.product_variant_ids])
- return res
+ @api.multi
+ @api.depends('product_variant_ids.sales_count')
+ def _sales_count(self):
+ for product in self:
+ product.sales_count = sum([p.sales_count for p in product.product_variant_ids])
- def action_view_sales(self, cr, uid, ids, context=None):
- act_obj = self.pool.get('ir.actions.act_window')
- mod_obj = self.pool.get('ir.model.data')
- product_ids = []
- for template in self.browse(cr, uid, ids, context=context):
- product_ids += [x.id for x in template.product_variant_ids]
- result = mod_obj.xmlid_to_res_id(cr, uid, 'sale.action_order_line_product_tree',raise_if_not_found=True)
- result = act_obj.read(cr, uid, [result], context=context)[0]
- result['domain'] = "[('product_id','in',[" + ','.join(map(str, product_ids)) + "])]"
- return result
+ @api.multi
+ def action_view_sales(self):
+ self.ensure_one()
+ action = self.env.ref('sale.action_product_sale_list')
+ product_ids = self.product_variant_ids.ids
+ return {
+ 'name': action.name,
+ 'help': action.help,
+ 'type': action.type,
+ 'view_type': action.view_type,
+ 'view_mode': action.view_mode,
+ 'target': action.target,
+ 'context': "{'search_default_product_id': " + ','.join(map(str, product_ids)) + ", 'default_product_id': " + str(product_ids[0]) + "}",
+ 'res_model': action.res_model,
+ 'domain': action.domain,
+ }
- _columns = {
- 'sales_count': fields.function(_sales_count, string='# Sales', type='integer'),
-
- }
+ sales_count = fields.Integer(compute='_sales_count', string='# Sales')
+ invoice_policy = fields.Selection(
+ [('order', 'Ordered quantities'),
+ ('delivery', 'Delivered quantities'),
+ ('cost', 'Invoice at cost (time and material, expenses)')],
+ string='Invoicing Policy', default='order')
diff --git a/addons/sale/sale_analytic.py b/addons/sale/sale_analytic.py
new file mode 100644
index 00000000000..1910320ca8f
--- /dev/null
+++ b/addons/sale/sale_analytic.py
@@ -0,0 +1,108 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from openerp import api, fields, models
+
+
+class SaleOrderLine(models.Model):
+ _inherit = "sale.order.line"
+
+ @api.multi
+ def _compute_analytic(self, domain=None):
+ lines = {}
+ if not domain:
+ domain = [('so_line', 'in', self.ids), ('amount', '<', 0.0)]
+ data = self.env['account.analytic.line'].read_group(
+ domain,
+ ['so_line', 'unit_amount', 'product_uom_id'], ['product_uom_id', 'so_line'], lazy=False
+ )
+ for d in data:
+ if not d['product_uom_id']:
+ continue
+ line = self.browse(d['so_line'][0])
+ lines.setdefault(line, 0.0)
+ uom = self.env['product.uom'].browse(d['product_uom_id'][0])
+
+ qty = self.env['product.uom']._compute_qty_obj(uom, d['unit_amount'], line.product_uom)
+ lines[line] += qty
+
+ for line, qty in lines.items():
+ line.qty_delivered = qty
+ return True
+
+
+class AccountAnalyticLine(models.Model):
+ _inherit = "account.analytic.line"
+ so_line = fields.Many2one('sale.order.line', string='Sale Order Line')
+
+ def _get_invoice_price(self, order):
+ return abs(self.amount / self.unit_amount)
+
+ def _get_sale_order_line_vals(self):
+ order = self.env['sale.order'].search([('project_id', '=', self.account_id.id), ('state', '=', 'sale')], limit=1)
+
+ last_so_line = self.env['sale.order.line'].search([('order_id', '=', order.id)], order='sequence desc', limit=1)
+ last_sequence = last_so_line.sequence + 1 if last_so_line else 100
+
+ fpos = order.fiscal_position_id or order.partner_id.property_account_position_id
+ taxes = fpos.map_tax(self.product_id.taxes_id)
+ price = self._get_invoice_price(order)
+
+ return {
+ 'order_id': order.id,
+ 'name': self.name,
+ 'sequence': last_sequence,
+ 'price_unit': price,
+ 'tax_id': [x.id for x in taxes],
+ 'discount': 0.0,
+ 'product_id': self.product_id.id,
+ 'product_uom': self.product_uom_id.id,
+ 'product_uom_qty': 0.0,
+ 'qty_delivered': self.unit_amount,
+ }
+
+ def _get_sale_order_line(self, vals=None):
+ result = dict(vals or {})
+ sol = result.get('so_line', False) or self.so_line
+ if not sol and self.account_id and self.product_id and self.product_id.invoice_policy in ('cost', 'order'):
+ sol = self.env['sale.order.line'].search([
+ ('order_id.project_id', '=', self.account_id.id),
+ ('state', '=', 'sale'),
+ ('product_id', '=', self.product_id.id)],
+ limit=1)
+ result.update({'so_line': sol.id})
+
+ if not sol and self.account_id and self.product_id and self.product_id.invoice_policy == 'cost':
+ order_line_vals = self._get_sale_order_line_vals()
+ sol = self.env['sale.order.line'].create(order_line_vals)
+ sol._compute_tax_id()
+ result.update({'so_line': sol.id})
+
+ return result
+
+ @api.multi
+ def write(self, values):
+ if self._context.get('create', False):
+ return super(AccountAnalyticLine, self).write(values)
+
+ todo = self.mapped('so_line')
+ result = super(AccountAnalyticLine, self).write(values)
+ if 'so_line' in values:
+ todo |= self.mapped('so_line')
+
+ for line in self:
+ res = self._get_sale_order_line(vals=values)
+ super(AccountAnalyticLine, line).write(res)
+ if 'so_line' in res:
+ todo |= line.mapped('so_line')
+
+ todo._compute_analytic()
+ return result
+
+ @api.model
+ def create(self, values):
+ line = super(AccountAnalyticLine, self).create(values)
+ res = line._get_sale_order_line(vals=values)
+ line.with_context(create=True).write(res)
+ line.mapped('so_line')._compute_analytic()
+ return line
diff --git a/addons/sale/sale_demo.xml b/addons/sale/sale_demo.xml
index 9857cfe6ca0..2fabc306fcb 100644
--- a/addons/sale/sale_demo.xml
+++ b/addons/sale/sale_demo.xml
@@ -25,7 +25,6 @@
@@ -381,28 +391,37 @@
- Here is a list of each sales order line to be invoiced. You can - invoice sales orders partially, by lines of sales order. You do - not need this list if you invoice from the delivery orders or - if you invoice sales totally. -
-