##############################################################################
import time
+import pytz
+from openerp import SUPERUSER_ID, workflow
from datetime import datetime
from dateutil.relativedelta import relativedelta
from operator import attrgetter
from openerp.osv import fields, osv
-from openerp import pooler
from openerp.tools.translate import _
import openerp.addons.decimal_precision as dp
from openerp.osv.orm import browse_record, browse_null
]
_track = {
'state': {
- 'purchase.mt_rfq_confirmed': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'confirmed',
- 'purchase.mt_rfq_approved': lambda self, cr, uid, obj, ctx=None: obj['state'] == 'approved',
+ 'purchase.mt_rfq_confirmed': lambda self, cr, uid, obj, ctx=None: obj.state == 'confirmed',
+ 'purchase.mt_rfq_approved': lambda self, cr, uid, obj, ctx=None: obj.state == 'approved',
+ 'purchase.mt_rfq_done': lambda self, cr, uid, obj, ctx=None: obj.state == 'done',
},
}
_columns = {
'invoiced_rate': fields.function(_invoiced_rate, string='Invoiced', type='float'),
'invoice_method': fields.selection([('manual','Based on Purchase Order lines'),('order','Based on generated draft invoice'),('picking','Based on incoming shipments')], 'Invoicing Control', required=True,
readonly=True, states={'draft':[('readonly',False)], 'sent':[('readonly',False)]},
- help="Based on Purchase Order lines: place individual lines in 'Invoice Control > Based on P.O. lines' from where you can selectively create an invoice.\n" \
+ help="Based on Purchase Order lines: place individual lines in 'Invoice Control / On Purchase Order lines' from where you can selectively create an invoice.\n" \
"Based on generated invoice: create a draft invoice you can validate later.\n" \
- "Bases on incoming shipments: let you create an invoice when receptions are validated."
+ "Based on incoming shipments: let you create an invoice when receptions are validated."
),
'minimum_planned_date':fields.function(_minimum_planned_date, fnct_inv=_set_minimum_planned_date, string='Expected Date', type='date', select=True, help="This is computed as the minimum scheduled date of all purchase order lines' products.",
store = {
'create_uid': fields.many2one('res.users', 'Responsible'),
'company_id': fields.many2one('res.company','Company',required=True,select=1, states={'confirmed':[('readonly',True)], 'approved':[('readonly',True)]}),
'journal_id': fields.many2one('account.journal', 'Journal'),
-
}
_defaults = {
'date_order': fields.date.context_today,
_name = "purchase.order"
_inherit = ['mail.thread', 'ir.needaction_mixin']
_description = "Purchase Order"
- _order = "name desc"
+ _order = 'date_order desc, id desc'
def create(self, cr, uid, vals, context=None):
if vals.get('name','/')=='/':
vals['name'] = self.pool.get('ir.sequence').get(cr, uid, 'purchase.order') or '/'
+ if context is None:
+ context = {}
+ context.update({'mail_create_nolog': True})
order = super(purchase_order, self).create(cr, uid, vals, context=context)
+ self.message_post(cr, uid, [order], body=_("RFQ created"), context=context)
return order
def unlink(self, cr, uid, ids, context=None):
pick_ids += [picking.id for picking in po.picking_ids]
action_model, action_id = tuple(mod_obj.get_object_reference(cr, uid, 'stock', 'action_picking_tree4'))
- action = self.pool.get(action_model).read(cr, uid, action_id, context=context)
+ action = self.pool[action_model].read(cr, uid, action_id, context=context)
ctx = eval(action['context'])
ctx.update({
'search_default_purchase_id': ids[0]
'default_composition_mode': 'comment',
})
return {
+ 'name': _('Compose Email'),
'type': 'ir.actions.act_window',
'view_type': 'form',
'view_mode': 'form',
self.write(cr, uid, [id], {'state' : 'confirmed', 'validator' : uid})
return True
+ def _choose_account_from_po_line(self, cr, uid, po_line, context=None):
+ fiscal_obj = self.pool.get('account.fiscal.position')
+ property_obj = self.pool.get('ir.property')
+ if po_line.product_id:
+ acc_id = po_line.product_id.property_account_expense.id
+ if not acc_id:
+ acc_id = po_line.product_id.categ_id.property_account_expense_categ.id
+ if not acc_id:
+ raise osv.except_osv(_('Error!'), _('Define expense account for this company: "%s" (id:%d).') % (po_line.product_id.name, po_line.product_id.id,))
+ else:
+ acc_id = property_obj.get(cr, uid, 'property_account_expense_categ', 'product.category', context=context).id
+ fpos = po_line.order_id.fiscal_position or False
+ return fiscal_obj.map_account(cr, uid, fpos, acc_id)
+
def _prepare_inv_line(self, cr, uid, account_id, order_line, context=None):
"""Collects require data from purchase order line that is used to create invoice line
for that purchase order line
'uos_id': order_line.product_uom.id or False,
'invoice_line_tax_id': [(6, 0, [x.id for x in order_line.taxes_id])],
'account_analytic_id': order_line.account_analytic_id.id or False,
+ 'purchase_line_id': order_line.id,
}
def action_cancel_draft(self, cr, uid, ids, context=None):
:return: ID of created invoice.
:rtype: int
"""
- res = False
-
+ if context is None:
+ context = {}
journal_obj = self.pool.get('account.journal')
inv_obj = self.pool.get('account.invoice')
inv_line_obj = self.pool.get('account.invoice.line')
- fiscal_obj = self.pool.get('account.fiscal.position')
- property_obj = self.pool.get('ir.property')
+ res = False
+ uid_company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
for order in self.browse(cr, uid, ids, context=context):
+ context.pop('force_company', None)
+ if order.company_id.id != uid_company_id:
+ #if the company of the document is different than the current user company, force the company in the context
+ #then re-do a browse to read the property fields for the good company.
+ context['force_company'] = order.company_id.id
+ order = self.browse(cr, uid, order.id, context=context)
pay_acc_id = order.partner_id.property_account_payable.id
- journal_ids = journal_obj.search(cr, uid, [('type', '=','purchase'),('company_id', '=', order.company_id.id)], limit=1)
+ journal_ids = journal_obj.search(cr, uid, [('type', '=', 'purchase'), ('company_id', '=', order.company_id.id)], limit=1)
if not journal_ids:
raise osv.except_osv(_('Error!'),
_('Define purchase journal for this company: "%s" (id:%d).') % (order.company_id.name, order.company_id.id))
# generate invoice line correspond to PO line and link that to created invoice (inv_id) and PO line
inv_lines = []
for po_line in order.order_line:
- if po_line.product_id:
- acc_id = po_line.product_id.property_account_expense.id
- if not acc_id:
- acc_id = po_line.product_id.categ_id.property_account_expense_categ.id
- if not acc_id:
- raise osv.except_osv(_('Error!'), _('Define expense account for this company: "%s" (id:%d).') % (po_line.product_id.name, po_line.product_id.id,))
- else:
- acc_id = property_obj.get(cr, uid, 'property_account_expense_categ', 'product.category').id
- fpos = order.fiscal_position or False
- acc_id = fiscal_obj.map_account(cr, uid, fpos, acc_id)
-
+ acc_id = self._choose_account_from_po_line(cr, uid, po_line, context=context)
inv_line_data = self._prepare_inv_line(cr, uid, acc_id, po_line, context=context)
inv_line_id = inv_line_obj.create(cr, uid, inv_line_data, context=context)
inv_lines.append(inv_line_id)
- po_line.write({'invoiced':True, 'invoice_lines': [(4, inv_line_id)]}, context=context)
+ po_line.write({'invoiced': True, 'invoice_lines': [(4, inv_line_id)]}, context=context)
# get invoice data and create invoice
inv_data = {
'account_id': pay_acc_id,
'type': 'in_invoice',
'partner_id': order.partner_id.id,
- 'currency_id': order.pricelist_id.currency_id.id,
+ 'currency_id': order.currency_id.id,
'journal_id': len(journal_ids) and journal_ids[0] or False,
'invoice_line': [(6, 0, inv_lines)],
'origin': order.name,
self.signal_purchase_cancel(cr, uid, ids)
return True
+ def date_to_datetime(self, cr, uid, userdate, context=None):
+ """ Convert date values expressed in user's timezone to
+ server-side UTC timestamp, assuming a default arbitrary
+ time of 12:00 AM - because a time is needed.
+
+ :param str userdate: date string in in user time zone
+ :return: UTC datetime string for server-side use
+ """
+ # TODO: move to fields.datetime in server after 7.0
+ user_date = datetime.strptime(userdate, DEFAULT_SERVER_DATE_FORMAT)
+ if context and context.get('tz'):
+ tz_name = context['tz']
+ else:
+ tz_name = self.pool.get('res.users').read(cr, SUPERUSER_ID, uid, ['tz'])['tz']
+ if tz_name:
+ utc = pytz.timezone('UTC')
+ context_tz = pytz.timezone(tz_name)
+ user_datetime = user_date + relativedelta(hours=12.0)
+ local_timestamp = context_tz.localize(user_datetime, is_dst=False)
+ user_datetime = local_timestamp.astimezone(utc)
+ return user_datetime.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
+ return user_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
+
def _prepare_order_picking(self, cr, uid, order, context=None):
return {
'name': self.pool.get('ir.sequence').get(cr, uid, 'stock.picking.in'),
'origin': order.name + ((order.origin and (':' + order.origin)) or ''),
- 'date': order.date_order,
+ 'date': self.date_to_datetime(cr, uid, order.date_order, context),
'partner_id': order.dest_address_id.id or order.partner_id.id,
'invoice_state': '2binvoiced' if order.invoice_method == 'picking' else 'none',
'type': 'in',
}
def _prepare_order_line_move(self, cr, uid, order, order_line, picking_id, context=None):
+ ''' prepare the stock move data from the PO line '''
return {
'name': order_line.name or '',
'product_id': order_line.product_id.id,
'product_uos_qty': order_line.product_qty,
'product_uom': order_line.product_uom.id,
'product_uos': order_line.product_uom.id,
- 'date': order_line.date_planned,
- 'date_expected': order_line.date_planned,
+ 'date': self.date_to_datetime(cr, uid, order.date_order, context),
+ 'date_expected': self.date_to_datetime(cr, uid, order_line.date_planned, context),
'location_id': order.partner_id.property_stock_supplier.id,
'location_dest_id': order.location_id.id,
'picking_id': picking_id,
continue
if order_line.product_id.type in ('product', 'consu'):
move = stock_move.create(cr, uid, self._prepare_order_line_move(cr, uid, order, order_line, picking_id, context=context))
- if order_line.move_dest_id:
+ if order_line.move_dest_id and order_line.move_dest_id.state != 'done':
order_line.move_dest_id.write({'location_id': order.location_id.id})
todo_moves.append(move)
stock_move.action_confirm(cr, uid, todo_moves)
def picking_done(self, cr, uid, ids, context=None):
self.write(cr, uid, ids, {'shipped':1,'state':'approved'}, context=context)
+ self.message_post(cr, uid, ids, body=_("Products received"), context=context)
return True
def copy(self, cr, uid, id, default=None, context=None):
'invoiced':False,
'invoice_ids': [],
'picking_ids': [],
+ 'partner_ref': '',
'name': self.pool.get('ir.sequence').get(cr, uid, 'purchase.order'),
})
return super(purchase_order, self).copy(cr, uid, id, default, context)
list_key.sort()
return tuple(list_key)
+ if context is None:
+ context = {}
+
# Compute what the new orders should contain
new_orders = {}
order_data['order_line'] = [(0, 0, value) for value in order_data['order_line'].itervalues()]
# create the new order
+ context.update({'mail_create_nolog': True})
neworder_id = self.create(cr, uid, order_data)
+ self.message_post(cr, uid, [neworder_id], body=_("RFQ created"), context=context)
orders_info.update({neworder_id: old_ids})
allorders.append(neworder_id)
res[line.id] = cur_obj.round(cr, uid, cur, taxes['total'])
return res
+ def _get_uom_id(self, cr, uid, context=None):
+ 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
+
_columns = {
'name': fields.text('Description', required=True),
'product_qty': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
'invoice_lines': fields.many2many('account.invoice.line', 'purchase_order_line_invoice_rel', 'order_line_id', 'invoice_id', 'Invoice Lines', readonly=True),
'invoiced': fields.boolean('Invoiced', readonly=True),
'partner_id': fields.related('order_id','partner_id',string='Partner',readonly=True,type="many2one", relation="res.partner", store=True),
- 'date_order': fields.related('order_id','date_order',string='Order Date',readonly=True,type="date")
+ 'date_order': fields.related('order_id','date_order',string='Order Date',readonly=True,type="date"),
}
_defaults = {
+ 'product_uom' : _get_uom_id,
'product_qty': lambda *a: 1.0,
'state': lambda *args: 'draft',
'invoiced': lambda *a: 0,
default.update({'state':'draft', 'move_ids':[],'invoiced':0,'invoice_lines':[]})
return super(purchase_order_line, self).copy_data(cr, uid, id, default, context)
+ def unlink(self, cr, uid, ids, context=None):
+ procurement_ids_to_cancel = []
+ for line in self.browse(cr, uid, ids, context=context):
+ if line.move_dest_id:
+ procurement_ids_to_cancel.extend(procurement.id for procurement in line.move_dest_id.procurements)
+ if procurement_ids_to_cancel:
+ self.pool['procurement.order'].action_cancel(cr, uid, procurement_ids_to_cancel)
+ return super(purchase_order_line, self).unlink(cr, uid, ids, context=context)
+
def onchange_product_uom(self, cr, uid, ids, pricelist_id, product_id, qty, uom_id,
partner_id, date_order=False, fiscal_position_id=False, date_planned=False,
name=False, price_unit=False, context=None):
"""
onchange handler of product_uom.
"""
+ if context is None:
+ context = {}
if not uom_id:
return {'value': {'price_unit': price_unit or 0.0, 'name': name or '', 'product_uom' : uom_id or False}}
+ context = dict(context, purchase_uom_check=True)
return self.onchange_product_id(cr, uid, ids, pricelist_id, product_id, qty, uom_id,
partner_id, date_order=date_order, fiscal_position_id=fiscal_position_id, date_planned=date_planned,
name=name, price_unit=price_unit, context=context)
product_product = self.pool.get('product.product')
product_uom = self.pool.get('product.uom')
res_partner = self.pool.get('res.partner')
- product_supplierinfo = self.pool.get('product.supplierinfo')
product_pricelist = self.pool.get('product.pricelist')
account_fiscal_position = self.pool.get('account.fiscal.position')
account_tax = self.pool.get('account.tax')
lang = res_partner.browse(cr, uid, partner_id).lang
context_partner.update( {'lang': lang, 'partner_id': partner_id} )
product = product_product.browse(cr, uid, product_id, context=context_partner)
- name = product.name
+ #call name_get() with partner in the context to eventually match name and description in the seller_ids field
+ dummy, name = product_product.name_get(cr, uid, product_id, context=context_partner)[0]
if product.description_purchase:
name += '\n' + product.description_purchase
res['value'].update({'name': name})
uom_id = product_uom_po_id
if product.uom_id.category_id.id != product_uom.browse(cr, uid, uom_id, context=context).category_id.id:
- if self._check_product_uom_group(cr, uid, context=context):
+ if context.get('purchase_uom_check') and self._check_product_uom_group(cr, uid, context=context):
res['warning'] = {'title': _('Warning!'), 'message': _('Selected Unit of Measure does not belong to the same category as the product Unit of Measure.')}
uom_id = product_uom_po_id
self.write(cr, uid, ids, {'state': 'confirmed'}, context=context)
return True
-purchase_order_line()
class procurement_order(osv.osv):
_inherit = 'procurement.order'
partner_obj = self.pool.get('res.partner')
user = self.pool.get('res.users').browse(cr, uid, uid, context=context)
for procurement in self.browse(cr, uid, ids, context=context):
- if not procurement.product_id.seller_ids:
- message = _('No supplier defined for this product !')
- self.message_post(cr, uid, [procurement.id], body=message)
- cr.execute('update procurement_order set message=%s where id=%s', (message, procurement.id))
- return False
+ message = ''
partner = procurement.product_id.seller_id #Taken Main Supplier of Product of Procurement.
- if not partner:
+ if not procurement.product_id.seller_ids:
+ message = _('No supplier defined for this product !')
+ elif not partner:
message = _('No default supplier defined for this product')
- self.message_post(cr, uid, [procurement.id], body=message)
- cr.execute('update procurement_order set message=%s where id=%s', (message, procurement.id))
+ elif not partner_obj.address_get(cr, uid, [partner.id], ['delivery'])['delivery']:
+ message = _('No address defined for the supplier')
+
+ if message:
+ if procurement.message != message:
+ cr.execute('update procurement_order set message=%s where id=%s', (message, procurement.id))
return False
+
if user.company_id and user.company_id.partner_id:
if partner.id == user.company_id.partner_id.id:
raise osv.except_osv(_('Configuration Error!'), _('The product "%s" has been defined with your company as reseller which seems to be a configuration error!' % procurement.product_id.name))
- address_id = partner_obj.address_get(cr, uid, [partner.id], ['delivery'])['delivery']
- if not address_id:
- message = _('No address defined for the supplier')
- self.message_post(cr, uid, [procurement.id], body=message)
- cr.execute('update procurement_order set message=%s where id=%s', (message, procurement.id))
- return False
return True
seller_delay = int(procurement.product_id.seller_delay)
return schedule_date - relativedelta(days=seller_delay)
+ def _get_warehouse(self, procurement, user_company):
+ """
+ Return the warehouse containing the procurment stock location (or one of it ancestors)
+ If none match, returns then first warehouse of the company
+ """
+ # TODO refactor the domain once we implement the "parent_of" domain operator
+ # NOTE This method has been copied in the `purchase_requisition` module to ensure
+ # retro-compatibility. This code duplication will be deleted in next stable version.
+ # Do not forget to update both version in case of modification.
+ company_id = (procurement.company_id or user_company).id
+ domains = [
+ [
+ '&', ('company_id', '=', company_id),
+ '|', '&', ('lot_stock_id.parent_left', '<', procurement.location_id.parent_left),
+ ('lot_stock_id.parent_right', '>', procurement.location_id.parent_right),
+ ('lot_stock_id', '=', procurement.location_id.id)
+ ],
+ [('company_id', '=', company_id)]
+ ]
+
+ cr, uid = procurement._cr, procurement._uid
+ context = procurement._context
+ Warehouse = self.pool['stock.warehouse']
+ for domain in domains:
+ ids = Warehouse.search(cr, uid, domain, context=context)
+ if ids:
+ return ids[0]
+ return False
+
def make_po(self, cr, uid, ids, context=None):
""" Make purchase order from procurement
@return: New created Purchase Orders procurement wise
prod_obj = self.pool.get('product.product')
acc_pos_obj = self.pool.get('account.fiscal.position')
seq_obj = self.pool.get('ir.sequence')
- warehouse_obj = self.pool.get('stock.warehouse')
for procurement in self.browse(cr, uid, ids, context=context):
res_id = procurement.move_id.id
partner = procurement.product_id.seller_id # Taken Main Supplier of Product of Procurement.
partner_id = partner.id
address_id = partner_obj.address_get(cr, uid, [partner_id], ['delivery'])['delivery']
pricelist_id = partner.property_product_pricelist_purchase.id
- warehouse_id = warehouse_obj.search(cr, uid, [('company_id', '=', procurement.company_id.id or company.id)], context=context)
uom_id = procurement.product_id.uom_po_id.id
qty = uom_obj._compute_qty(cr, uid, procurement.product_uom.id, procurement.product_qty, uom_id)
'origin': procurement.origin,
'partner_id': partner_id,
'location_id': procurement.location_id.id,
- 'warehouse_id': warehouse_id and warehouse_id[0] or False,
+ 'warehouse_id': self._get_warehouse(procurement, company),
'pricelist_id': pricelist_id,
'date_order': purchase_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
'company_id': procurement.company_id.id,
self.pool.get('purchase.order').signal_send_rfq(cr, uid, [context['default_res_id']])
return super(mail_compose_message, self).send_mail(cr, uid, ids, context=context)
+
+class account_invoice(osv.Model):
+ """ Override account_invoice to add Chatter messages on the related purchase
+ orders, logging the invoice reception or payment. """
+ _inherit = 'account.invoice'
+
+ def invoice_validate(self, cr, uid, ids, context=None):
+ res = super(account_invoice, self).invoice_validate(cr, uid, ids, context=context)
+ purchase_order_obj = self.pool.get('purchase.order')
+ # read access on purchase.order object is not required
+ if not purchase_order_obj.check_access_rights(cr, uid, 'read', raise_exception=False):
+ user_id = SUPERUSER_ID
+ else:
+ user_id = uid
+ po_ids = purchase_order_obj.search(cr, user_id, [('invoice_ids', 'in', ids)], context=context)
+ for po_id in po_ids:
+ purchase_order_obj.message_post(cr, user_id, po_id, body=_("Invoice received"), context=context)
+ workflow.trg_write(uid, 'purchase.order', po_id, cr)
+ return res
+
+ def confirm_paid(self, cr, uid, ids, context=None):
+ res = super(account_invoice, self).confirm_paid(cr, uid, ids, context=context)
+ purchase_order_obj = self.pool.get('purchase.order')
+ # read access on purchase.order object is not required
+ if not purchase_order_obj.check_access_rights(cr, uid, 'read', raise_exception=False):
+ user_id = SUPERUSER_ID
+ else:
+ user_id = uid
+ po_ids = purchase_order_obj.search(cr, user_id, [('invoice_ids', 'in', ids)], context=context)
+ for po_id in po_ids:
+ purchase_order_obj.message_post(cr, user_id, po_id, body=_("Invoice paid"), context=context)
+ return res
+
+class account_invoice_line(osv.Model):
+ """ Override account_invoice_line to add the link to the purchase order line it is related to"""
+ _inherit = 'account.invoice.line'
+ _columns = {
+ 'purchase_line_id': fields.many2one('purchase.order.line',
+ 'Purchase Order Line', ondelete='set null', select=True,
+ readonly=True),
+ }
+
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: