The system showed different Description for product in invoice when you generate invoices based on delivery orders compared with based on the sales order lines. But the invoice is in the two cases attached with the SO. Then the description of the product must be the description linked to the SO line. opw:660723
364 lines
17 KiB
Python
364 lines
17 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 openerp.osv import fields, osv
|
|
|
|
class stock_location_path(osv.osv):
|
|
_inherit = "stock.location.path"
|
|
_columns = {
|
|
'invoice_state': fields.selection([
|
|
("invoiced", "Invoiced"),
|
|
("2binvoiced", "To Be Invoiced"),
|
|
("none", "Not Applicable")], "Invoice Status",),
|
|
}
|
|
_defaults = {
|
|
'invoice_state': '',
|
|
}
|
|
|
|
def _prepare_push_apply(self, cr, uid, rule, move, context=None):
|
|
res = super(stock_location_path, self)._prepare_push_apply(cr, uid, rule, move, context=context)
|
|
res['invoice_state'] = rule.invoice_state or 'none'
|
|
return res
|
|
|
|
#----------------------------------------------------------
|
|
# Procurement Rule
|
|
#----------------------------------------------------------
|
|
class procurement_rule(osv.osv):
|
|
_inherit = 'procurement.rule'
|
|
_columns = {
|
|
'invoice_state': fields.selection([
|
|
("invoiced", "Invoiced"),
|
|
("2binvoiced", "To Be Invoiced"),
|
|
("none", "Not Applicable")], "Invoice Status",),
|
|
}
|
|
_defaults = {
|
|
'invoice_state': '',
|
|
}
|
|
|
|
#----------------------------------------------------------
|
|
# Procurement Order
|
|
#----------------------------------------------------------
|
|
|
|
|
|
class procurement_order(osv.osv):
|
|
_inherit = "procurement.order"
|
|
_columns = {
|
|
'invoice_state': fields.selection([("invoiced", "Invoiced"),
|
|
("2binvoiced", "To Be Invoiced"),
|
|
("none", "Not Applicable")
|
|
], "Invoice Control"),
|
|
}
|
|
|
|
def _run_move_create(self, cr, uid, procurement, context=None):
|
|
res = super(procurement_order, self)._run_move_create(cr, uid, procurement, context=context)
|
|
res.update({'invoice_state': procurement.rule_id.invoice_state or procurement.invoice_state or 'none'})
|
|
return res
|
|
|
|
_defaults = {
|
|
'invoice_state': ''
|
|
}
|
|
|
|
|
|
#----------------------------------------------------------
|
|
# Move
|
|
#----------------------------------------------------------
|
|
|
|
class stock_move(osv.osv):
|
|
_inherit = "stock.move"
|
|
_columns = {
|
|
'invoice_state': fields.selection([("invoiced", "Invoiced"),
|
|
("2binvoiced", "To Be Invoiced"),
|
|
("none", "Not Applicable")], "Invoice Control",
|
|
select=True, required=True, track_visibility='onchange',
|
|
states={'draft': [('readonly', False)]}),
|
|
}
|
|
_defaults = {
|
|
'invoice_state': lambda *args, **argv: 'none'
|
|
}
|
|
|
|
def _get_master_data(self, cr, uid, move, company, context=None):
|
|
''' returns a tuple (browse_record(res.partner), ID(res.users), ID(res.currency)'''
|
|
currency = company.currency_id.id
|
|
partner = move.picking_id and move.picking_id.partner_id
|
|
if partner:
|
|
code = self.get_code_from_locs(cr, uid, move, context=context)
|
|
if partner.property_product_pricelist and code == 'outgoing':
|
|
currency = partner.property_product_pricelist.currency_id.id
|
|
return partner, uid, currency
|
|
|
|
def _create_invoice_line_from_vals(self, cr, uid, move, invoice_line_vals, context=None):
|
|
return self.pool.get('account.invoice.line').create(cr, uid, invoice_line_vals, context=context)
|
|
|
|
def _get_price_unit_invoice(self, cr, uid, move_line, type, context=None):
|
|
""" Gets price unit for invoice
|
|
@param move_line: Stock move lines
|
|
@param type: Type of invoice
|
|
@return: The price unit for the move line
|
|
"""
|
|
if context is None:
|
|
context = {}
|
|
if type in ('in_invoice', 'in_refund'):
|
|
return move_line.price_unit
|
|
else:
|
|
# If partner given, search price in its sale pricelist
|
|
if move_line.partner_id and move_line.partner_id.property_product_pricelist:
|
|
pricelist_obj = self.pool.get("product.pricelist")
|
|
pricelist = move_line.partner_id.property_product_pricelist.id
|
|
price = pricelist_obj.price_get(cr, uid, [pricelist],
|
|
move_line.product_id.id, move_line.product_uom_qty, move_line.partner_id.id, {
|
|
'uom': move_line.product_uom.id,
|
|
'date': move_line.date,
|
|
})[pricelist]
|
|
if price:
|
|
return price
|
|
return move_line.product_id.lst_price
|
|
|
|
def _get_invoice_line_vals(self, cr, uid, move, partner, inv_type, context=None):
|
|
fp_obj = self.pool.get('account.fiscal.position')
|
|
# Get account_id
|
|
fp = fp_obj.browse(cr, uid, context.get('fp_id')) if context.get('fp_id') else False
|
|
name = False
|
|
if inv_type in ('out_invoice', 'out_refund'):
|
|
account_id = move.product_id.property_account_income.id
|
|
if not account_id:
|
|
account_id = move.product_id.categ_id.property_account_income_categ.id
|
|
if move.procurement_id and move.procurement_id.sale_line_id:
|
|
name = move.procurement_id.sale_line_id.name
|
|
else:
|
|
account_id = move.product_id.property_account_expense.id
|
|
if not account_id:
|
|
account_id = move.product_id.categ_id.property_account_expense_categ.id
|
|
fiscal_position = fp or partner.property_account_position
|
|
account_id = fp_obj.map_account(cr, uid, fiscal_position, account_id)
|
|
|
|
# set UoS if it's a sale and the picking doesn't have one
|
|
uos_id = move.product_uom.id
|
|
quantity = move.product_uom_qty
|
|
if move.product_uos:
|
|
uos_id = move.product_uos.id
|
|
quantity = move.product_uos_qty
|
|
|
|
taxes_ids = self._get_taxes(cr, uid, move, context=context)
|
|
|
|
return {
|
|
'name': name or move.name,
|
|
'account_id': account_id,
|
|
'product_id': move.product_id.id,
|
|
'uos_id': uos_id,
|
|
'quantity': quantity,
|
|
'price_unit': self._get_price_unit_invoice(cr, uid, move, inv_type),
|
|
'invoice_line_tax_id': [(6, 0, taxes_ids)],
|
|
'discount': 0.0,
|
|
'account_analytic_id': False,
|
|
}
|
|
|
|
def _get_moves_taxes(self, cr, uid, moves, inv_type, context=None):
|
|
#extra moves with the same picking_id and product_id of a move have the same taxes
|
|
extra_move_tax = {}
|
|
is_extra_move = {}
|
|
for move in moves:
|
|
if move.picking_id:
|
|
is_extra_move[move.id] = True
|
|
if not (move.picking_id, move.product_id) in extra_move_tax:
|
|
extra_move_tax[move.picking_id, move.product_id] = 0
|
|
else:
|
|
is_extra_move[move.id] = False
|
|
return (is_extra_move, extra_move_tax)
|
|
|
|
def action_cancel(self, cr, uid, ids, context=None):
|
|
res = super(stock_move, self).action_cancel(cr, uid, ids, context=context)
|
|
self.write(cr, uid, ids, {'invoice_state': 'none'}, context=context)
|
|
return res
|
|
|
|
#----------------------------------------------------------
|
|
# Picking
|
|
#----------------------------------------------------------
|
|
|
|
class stock_picking(osv.osv):
|
|
_inherit = 'stock.picking'
|
|
def __get_invoice_state(self, cr, uid, ids, name, arg, context=None):
|
|
result = {}
|
|
for pick in self.browse(cr, uid, ids, context=context):
|
|
result[pick.id] = 'none'
|
|
for move in pick.move_lines:
|
|
if move.invoice_state == 'invoiced':
|
|
result[pick.id] = 'invoiced'
|
|
elif move.invoice_state == '2binvoiced':
|
|
result[pick.id] = '2binvoiced'
|
|
break
|
|
return result
|
|
|
|
def __get_picking_move(self, cr, uid, ids, context={}):
|
|
res = []
|
|
for move in self.pool.get('stock.move').browse(cr, uid, ids, context=context):
|
|
if move.picking_id and move.invoice_state != move.picking_id.invoice_state:
|
|
res.append(move.picking_id.id)
|
|
return res
|
|
|
|
def _set_inv_state(self, cr, uid, picking_id, name, value, arg, context=None):
|
|
pick = self.browse(cr, uid, picking_id, context=context)
|
|
moves = [x.id for x in pick.move_lines]
|
|
move_obj= self.pool.get("stock.move")
|
|
move_obj.write(cr, uid, moves, {'invoice_state': value}, context=context)
|
|
|
|
_columns = {
|
|
'invoice_state': fields.function(__get_invoice_state, type='selection', selection=[
|
|
("invoiced", "Invoiced"),
|
|
("2binvoiced", "To Be Invoiced"),
|
|
("none", "Not Applicable")
|
|
], string="Invoice Control", required=True,
|
|
fnct_inv = _set_inv_state,
|
|
store={
|
|
'stock.picking': (lambda self, cr, uid, ids, c={}: ids, ['state'], 10),
|
|
'stock.move': (__get_picking_move, ['picking_id', 'invoice_state'], 10),
|
|
},
|
|
),
|
|
}
|
|
_defaults = {
|
|
'invoice_state': lambda *args, **argv: 'none'
|
|
}
|
|
|
|
def _create_invoice_from_picking(self, cr, uid, picking, vals, context=None):
|
|
''' This function simply creates the invoice from the given values. It is overriden in delivery module to add the delivery costs.
|
|
'''
|
|
invoice_obj = self.pool.get('account.invoice')
|
|
return invoice_obj.create(cr, uid, vals, context=context)
|
|
|
|
def _get_partner_to_invoice(self, cr, uid, picking, context=None):
|
|
""" Gets the partner that will be invoiced
|
|
Note that this function is inherited in the sale and purchase modules
|
|
@param picking: object of the picking for which we are selecting the partner to invoice
|
|
@return: object of the partner to invoice
|
|
"""
|
|
return picking.partner_id and picking.partner_id.id
|
|
|
|
def action_invoice_create(self, cr, uid, ids, journal_id, group=False, type='out_invoice', context=None):
|
|
""" Creates invoice based on the invoice state selected for picking.
|
|
@param journal_id: Id of journal
|
|
@param group: Whether to create a group invoice or not
|
|
@param type: Type invoice to be created
|
|
@return: Ids of created invoices for the pickings
|
|
"""
|
|
context = context or {}
|
|
todo = {}
|
|
for picking in self.browse(cr, uid, ids, context=context):
|
|
partner = self._get_partner_to_invoice(cr, uid, picking, dict(context, type=type))
|
|
#grouping is based on the invoiced partner
|
|
if group:
|
|
key = partner
|
|
else:
|
|
key = picking.id
|
|
for move in picking.move_lines:
|
|
if move.invoice_state == '2binvoiced':
|
|
if (move.state != 'cancel') and not move.scrapped:
|
|
todo.setdefault(key, [])
|
|
todo[key].append(move)
|
|
invoices = []
|
|
for moves in todo.values():
|
|
invoices += self._invoice_create_line(cr, uid, moves, journal_id, type, context=context)
|
|
return invoices
|
|
|
|
def _get_invoice_vals(self, cr, uid, key, inv_type, journal_id, move, context=None):
|
|
if context is None:
|
|
context = {}
|
|
partner, currency_id, company_id, user_id = key
|
|
if inv_type in ('out_invoice', 'out_refund'):
|
|
account_id = partner.property_account_receivable.id
|
|
payment_term = partner.property_payment_term.id or False
|
|
else:
|
|
account_id = partner.property_account_payable.id
|
|
payment_term = partner.property_supplier_payment_term.id or False
|
|
return {
|
|
'origin': move.picking_id.name,
|
|
'date_invoice': context.get('date_inv', False),
|
|
'user_id': user_id,
|
|
'partner_id': partner.id,
|
|
'account_id': account_id,
|
|
'payment_term': payment_term,
|
|
'type': inv_type,
|
|
'fiscal_position': partner.property_account_position.id,
|
|
'company_id': company_id,
|
|
'currency_id': currency_id,
|
|
'journal_id': journal_id,
|
|
}
|
|
|
|
def _invoice_create_line(self, cr, uid, moves, journal_id, inv_type='out_invoice', context=None):
|
|
invoice_obj = self.pool.get('account.invoice')
|
|
move_obj = self.pool.get('stock.move')
|
|
invoices = {}
|
|
is_extra_move, extra_move_tax = move_obj._get_moves_taxes(cr, uid, moves, inv_type, context=context)
|
|
product_price_unit = {}
|
|
for move in moves:
|
|
company = move.company_id
|
|
origin = move.picking_id.name
|
|
partner, user_id, currency_id = move_obj._get_master_data(cr, uid, move, company, context=context)
|
|
|
|
key = (partner, currency_id, company.id, user_id)
|
|
invoice_vals = self._get_invoice_vals(cr, uid, key, inv_type, journal_id, move, context=context)
|
|
|
|
if key not in invoices:
|
|
# Get account and payment terms
|
|
invoice_id = self._create_invoice_from_picking(cr, uid, move.picking_id, invoice_vals, context=context)
|
|
invoices[key] = invoice_id
|
|
else:
|
|
invoice = invoice_obj.browse(cr, uid, invoices[key], context=context)
|
|
merge_vals = {}
|
|
if not invoice.origin or invoice_vals['origin'] not in invoice.origin.split(', '):
|
|
invoice_origin = filter(None, [invoice.origin, invoice_vals['origin']])
|
|
merge_vals['origin'] = ', '.join(invoice_origin)
|
|
if invoice_vals.get('name', False) and (not invoice.name or invoice_vals['name'] not in invoice.name.split(', ')):
|
|
invoice_name = filter(None, [invoice.name, invoice_vals['name']])
|
|
merge_vals['name'] = ', '.join(invoice_name)
|
|
if merge_vals:
|
|
invoice.write(merge_vals)
|
|
invoice_line_vals = move_obj._get_invoice_line_vals(cr, uid, move, partner, inv_type, context=dict(context, fp_id=invoice_vals.get('fiscal_position', False)))
|
|
invoice_line_vals['invoice_id'] = invoices[key]
|
|
invoice_line_vals['origin'] = origin
|
|
if not is_extra_move[move.id]:
|
|
product_price_unit[invoice_line_vals['product_id'], invoice_line_vals['uos_id']] = invoice_line_vals['price_unit']
|
|
if is_extra_move[move.id] and (invoice_line_vals['product_id'], invoice_line_vals['uos_id']) in product_price_unit:
|
|
invoice_line_vals['price_unit'] = product_price_unit[invoice_line_vals['product_id'], invoice_line_vals['uos_id']]
|
|
if is_extra_move[move.id]:
|
|
desc = (inv_type in ('out_invoice', 'out_refund') and move.product_id.product_tmpl_id.description_sale) or \
|
|
(inv_type in ('in_invoice','in_refund') and move.product_id.product_tmpl_id.description_purchase)
|
|
invoice_line_vals['name'] += ' ' + desc if desc else ''
|
|
if extra_move_tax[move.picking_id, move.product_id]:
|
|
invoice_line_vals['invoice_line_tax_id'] = extra_move_tax[move.picking_id, move.product_id]
|
|
#the default product taxes
|
|
elif (0, move.product_id) in extra_move_tax:
|
|
invoice_line_vals['invoice_line_tax_id'] = extra_move_tax[0, move.product_id]
|
|
|
|
move_obj._create_invoice_line_from_vals(cr, uid, move, invoice_line_vals, context=context)
|
|
move_obj.write(cr, uid, move.id, {'invoice_state': 'invoiced'}, context=context)
|
|
|
|
invoice_obj.button_compute(cr, uid, invoices.values(), context=context, set_total=(inv_type in ('in_invoice', 'in_refund')))
|
|
return invoices.values()
|
|
|
|
def _prepare_values_extra_move(self, cr, uid, op, product, remaining_qty, context=None):
|
|
"""
|
|
Need to pass invoice_state of picking when an extra move is created which is not a copy of a previous
|
|
"""
|
|
res = super(stock_picking, self)._prepare_values_extra_move(cr, uid, op, product, remaining_qty, context=context)
|
|
res.update({'invoice_state': op.picking_id.invoice_state})
|
|
if op.linked_move_operation_ids:
|
|
res.update({'price_unit': op.linked_move_operation_ids[-1].move_id.price_unit})
|
|
return res
|