1180 lines
57 KiB
Python
1180 lines
57 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from datetime import datetime
|
|
from dateutil.relativedelta import relativedelta
|
|
|
|
from odoo import api, fields, models, SUPERUSER_ID, _
|
|
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
|
|
from odoo.tools.float_utils import float_is_zero, float_compare
|
|
from odoo.exceptions import UserError, AccessError
|
|
from odoo.tools.misc import formatLang
|
|
from odoo.addons.base.res.res_partner import WARNING_MESSAGE, WARNING_HELP
|
|
import odoo.addons.decimal_precision as dp
|
|
|
|
|
|
class PurchaseOrder(models.Model):
|
|
_name = "purchase.order"
|
|
_inherit = ['mail.thread', 'ir.needaction_mixin']
|
|
_description = "Purchase Order"
|
|
_order = 'date_order desc, id desc'
|
|
|
|
@api.depends('order_line.price_total')
|
|
def _amount_all(self):
|
|
for order in self:
|
|
amount_untaxed = amount_tax = 0.0
|
|
for line in order.order_line:
|
|
amount_untaxed += line.price_subtotal
|
|
# FORWARDPORT UP TO 10.0
|
|
if order.company_id.tax_calculation_rounding_method == 'round_globally':
|
|
taxes = line.taxes_id.compute_all(line.price_unit, line.order_id.currency_id, line.product_qty, product=line.product_id, partner=line.order_id.partner_id)
|
|
amount_tax += sum(t.get('amount', 0.0) for t in taxes.get('taxes', []))
|
|
else:
|
|
amount_tax += line.price_tax
|
|
order.update({
|
|
'amount_untaxed': order.currency_id.round(amount_untaxed),
|
|
'amount_tax': order.currency_id.round(amount_tax),
|
|
'amount_total': amount_untaxed + amount_tax,
|
|
})
|
|
|
|
@api.depends('order_line.date_planned')
|
|
def _compute_date_planned(self):
|
|
for order in self:
|
|
min_date = False
|
|
for line in order.order_line:
|
|
if not min_date or line.date_planned < min_date:
|
|
min_date = line.date_planned
|
|
if min_date:
|
|
order.date_planned = min_date
|
|
|
|
@api.depends('state', 'order_line.qty_invoiced', 'order_line.qty_received', 'order_line.product_qty')
|
|
def _get_invoiced(self):
|
|
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
|
for order in self:
|
|
if order.state not in ('purchase', 'done'):
|
|
order.invoice_status = 'no'
|
|
continue
|
|
|
|
if any(float_compare(line.qty_invoiced, line.product_qty if line.product_id.purchase_method == 'purchase' else line.qty_received, precision_digits=precision) == -1 for line in order.order_line):
|
|
order.invoice_status = 'to invoice'
|
|
elif all(float_compare(line.qty_invoiced, line.product_qty if line.product_id.purchase_method == 'purchase' else line.qty_received, precision_digits=precision) >= 0 for line in order.order_line) and order.invoice_ids:
|
|
order.invoice_status = 'invoiced'
|
|
else:
|
|
order.invoice_status = 'no'
|
|
|
|
@api.depends('order_line.invoice_lines.invoice_id.state')
|
|
def _compute_invoice(self):
|
|
for order in self:
|
|
invoices = self.env['account.invoice']
|
|
for line in order.order_line:
|
|
invoices |= line.invoice_lines.mapped('invoice_id')
|
|
order.invoice_ids = invoices
|
|
order.invoice_count = len(invoices)
|
|
|
|
@api.model
|
|
def _default_picking_type(self):
|
|
type_obj = self.env['stock.picking.type']
|
|
company_id = self.env.context.get('company_id') or self.env.user.company_id.id
|
|
types = type_obj.search([('code', '=', 'incoming'), ('warehouse_id.company_id', '=', company_id)])
|
|
if not types:
|
|
types = type_obj.search([('code', '=', 'incoming'), ('warehouse_id', '=', False)])
|
|
return types[:1]
|
|
|
|
@api.depends('order_line.move_ids')
|
|
def _compute_picking(self):
|
|
for order in self:
|
|
pickings = self.env['stock.picking']
|
|
for line in order.order_line:
|
|
# We keep a limited scope on purpose. Ideally, we should also use move_orig_ids and
|
|
# do some recursive search, but that could be prohibitive if not done correctly.
|
|
moves = line.move_ids | line.move_ids.mapped('returned_move_ids')
|
|
moves = moves.filtered(lambda r: r.state != 'cancel')
|
|
pickings |= moves.mapped('picking_id')
|
|
order.picking_ids = pickings
|
|
order.picking_count = len(pickings)
|
|
|
|
@api.depends('picking_ids', 'picking_ids.state')
|
|
def _compute_is_shipped(self):
|
|
for order in self:
|
|
if order.picking_ids and all([x.state == 'done' for x in order.picking_ids]):
|
|
order.is_shipped = True
|
|
|
|
READONLY_STATES = {
|
|
'purchase': [('readonly', True)],
|
|
'done': [('readonly', True)],
|
|
'cancel': [('readonly', True)],
|
|
}
|
|
|
|
name = fields.Char('Order Reference', required=True, index=True, copy=False, default='New')
|
|
origin = fields.Char('Source Document', copy=False,\
|
|
help="Reference of the document that generated this purchase order "
|
|
"request (e.g. a sale order or an internal procurement request)")
|
|
partner_ref = fields.Char('Vendor Reference', copy=False,\
|
|
help="Reference of the sales order or bid sent by the vendor. "
|
|
"It's used to do the matching when you receive the "
|
|
"products as this reference is usually written on the "
|
|
"delivery order sent by your vendor.")
|
|
date_order = fields.Datetime('Order Date', required=True, states=READONLY_STATES, index=True, copy=False, default=fields.Datetime.now,\
|
|
help="Depicts the date where the Quotation should be validated and converted into a purchase order.")
|
|
date_approve = fields.Date('Approval Date', readonly=1, index=True, copy=False)
|
|
partner_id = fields.Many2one('res.partner', string='Vendor', required=True, states=READONLY_STATES, change_default=True, track_visibility='always')
|
|
dest_address_id = fields.Many2one('res.partner', string='Drop Ship Address', states=READONLY_STATES,\
|
|
help="Put an address if you want to deliver directly from the vendor to the customer. "\
|
|
"Otherwise, keep empty to deliver to your own company.")
|
|
currency_id = fields.Many2one('res.currency', 'Currency', required=True, states=READONLY_STATES,\
|
|
default=lambda self: self.env.user.company_id.currency_id.id)
|
|
state = fields.Selection([
|
|
('draft', 'RFQ'),
|
|
('sent', 'RFQ Sent'),
|
|
('to approve', 'To Approve'),
|
|
('purchase', 'Purchase Order'),
|
|
('done', 'Locked'),
|
|
('cancel', 'Cancelled')
|
|
], string='Status', readonly=True, index=True, copy=False, default='draft', track_visibility='onchange')
|
|
order_line = fields.One2many('purchase.order.line', 'order_id', string='Order Lines', states={'cancel': [('readonly', True)], 'done': [('readonly', True)]}, copy=True)
|
|
notes = fields.Text('Terms and Conditions')
|
|
|
|
invoice_count = fields.Integer(compute="_compute_invoice", string='# of Bills', copy=False, default=0)
|
|
invoice_ids = fields.Many2many('account.invoice', compute="_compute_invoice", string='Bills', copy=False)
|
|
invoice_status = fields.Selection([
|
|
('no', 'Nothing to Bill'),
|
|
('to invoice', 'Waiting Bills'),
|
|
('invoiced', 'Bills Received'),
|
|
], string='Billing Status', compute='_get_invoiced', store=True, readonly=True, copy=False, default='no')
|
|
|
|
picking_count = fields.Integer(compute='_compute_picking', string='Receptions', default=0)
|
|
picking_ids = fields.Many2many('stock.picking', compute='_compute_picking', string='Receptions', copy=False)
|
|
|
|
# There is no inverse function on purpose since the date may be different on each line
|
|
date_planned = fields.Datetime(string='Scheduled Date', compute='_compute_date_planned', store=True, index=True)
|
|
|
|
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')
|
|
amount_total = fields.Monetary(string='Total', store=True, readonly=True, compute='_amount_all')
|
|
|
|
fiscal_position_id = fields.Many2one('account.fiscal.position', string='Fiscal Position', oldname='fiscal_position')
|
|
payment_term_id = fields.Many2one('account.payment.term', 'Payment Terms')
|
|
incoterm_id = fields.Many2one('stock.incoterms', 'Incoterm', states={'done': [('readonly', True)]}, help="International Commercial Terms are a series of predefined commercial terms used in international transactions.")
|
|
|
|
product_id = fields.Many2one('product.product', related='order_line.product_id', string='Product')
|
|
create_uid = fields.Many2one('res.users', 'Responsible')
|
|
company_id = fields.Many2one('res.company', 'Company', required=True, index=True, states=READONLY_STATES, default=lambda self: self.env.user.company_id.id)
|
|
|
|
picking_type_id = fields.Many2one('stock.picking.type', 'Deliver To', states=READONLY_STATES, required=True, default=_default_picking_type,\
|
|
help="This will determine picking type of incoming shipment")
|
|
default_location_dest_id_usage = fields.Selection(related='picking_type_id.default_location_dest_id.usage', string='Destination Location Type',\
|
|
help="Technical field used to display the Drop Ship Address", readonly=True)
|
|
group_id = fields.Many2one('procurement.group', string="Procurement Group", copy=False)
|
|
is_shipped = fields.Boolean(compute="_compute_is_shipped")
|
|
|
|
@api.model
|
|
def name_search(self, name, args=None, operator='ilike', limit=100):
|
|
args = args or []
|
|
domain = []
|
|
if name:
|
|
domain = ['|', ('name', operator, name), ('partner_ref', operator, name)]
|
|
pos = self.search(domain + args, limit=limit)
|
|
return pos.name_get()
|
|
|
|
@api.multi
|
|
@api.depends('name', 'partner_ref')
|
|
def name_get(self):
|
|
result = []
|
|
for po in self:
|
|
name = po.name
|
|
if po.partner_ref:
|
|
name += ' ('+po.partner_ref+')'
|
|
if po.amount_total:
|
|
name += ': ' + formatLang(self.env, po.amount_total, currency_obj=po.currency_id)
|
|
result.append((po.id, name))
|
|
return result
|
|
|
|
@api.model
|
|
def create(self, vals):
|
|
if vals.get('name', 'New') == 'New':
|
|
vals['name'] = self.env['ir.sequence'].next_by_code('purchase.order') or '/'
|
|
return super(PurchaseOrder, self).create(vals)
|
|
|
|
@api.multi
|
|
def unlink(self):
|
|
for order in self:
|
|
if not order.state == 'cancel':
|
|
raise UserError(_('In order to delete a purchase order, you must cancel it first.'))
|
|
return super(PurchaseOrder, self).unlink()
|
|
|
|
@api.multi
|
|
def copy(self, default=None):
|
|
new_po = super(PurchaseOrder, self).copy(default=default)
|
|
for line in new_po.order_line:
|
|
seller = line.product_id._select_seller(
|
|
partner_id=line.partner_id, quantity=line.product_qty,
|
|
date=line.order_id.date_order and line.order_id.date_order[:10], uom_id=line.product_uom)
|
|
line.date_planned = line._get_date_planned(seller)
|
|
return new_po
|
|
|
|
@api.multi
|
|
def _track_subtype(self, init_values):
|
|
self.ensure_one()
|
|
if 'state' in init_values and self.state == 'purchase':
|
|
return 'purchase.mt_rfq_approved'
|
|
elif 'state' in init_values and self.state == 'to approve':
|
|
return 'purchase.mt_rfq_confirmed'
|
|
elif 'state' in init_values and self.state == 'done':
|
|
return 'purchase.mt_rfq_done'
|
|
return super(PurchaseOrder, self)._track_subtype(init_values)
|
|
|
|
@api.onchange('partner_id', 'company_id')
|
|
def onchange_partner_id(self):
|
|
if not self.partner_id:
|
|
self.fiscal_position_id = False
|
|
self.payment_term_id = False
|
|
self.currency_id = False
|
|
else:
|
|
self.fiscal_position_id = self.env['account.fiscal.position'].with_context(company_id=self.company_id.id).get_fiscal_position(self.partner_id.id)
|
|
self.payment_term_id = self.partner_id.property_supplier_payment_term_id.id
|
|
self.currency_id = self.partner_id.property_purchase_currency_id.id or self.env.user.company_id.currency_id.id
|
|
return {}
|
|
|
|
@api.onchange('fiscal_position_id')
|
|
def _compute_tax_id(self):
|
|
"""
|
|
Trigger the recompute of the taxes if the fiscal position is changed on the PO.
|
|
"""
|
|
for order in self:
|
|
order.order_line._compute_tax_id()
|
|
|
|
@api.onchange('partner_id')
|
|
def onchange_partner_id_warning(self):
|
|
if not self.partner_id:
|
|
return
|
|
warning = {}
|
|
title = False
|
|
message = False
|
|
|
|
partner = self.partner_id
|
|
|
|
# If partner has no warning, check its company
|
|
if partner.purchase_warn == 'no-message' and partner.parent_id:
|
|
partner = partner.parent_id
|
|
|
|
if partner.purchase_warn != 'no-message':
|
|
# Block if partner only has warning but parent company is blocked
|
|
if partner.purchase_warn != 'block' and partner.parent_id and partner.parent_id.purchase_warn == 'block':
|
|
partner = partner.parent_id
|
|
title = _("Warning for %s") % partner.name
|
|
message = partner.purchase_warn_msg
|
|
warning = {
|
|
'title': title,
|
|
'message': message
|
|
}
|
|
if partner.purchase_warn == 'block':
|
|
self.update({'partner_id': False})
|
|
return {'warning': warning}
|
|
return {}
|
|
|
|
@api.onchange('picking_type_id')
|
|
def _onchange_picking_type_id(self):
|
|
if self.picking_type_id.default_location_dest_id.usage != 'customer':
|
|
self.dest_address_id = False
|
|
|
|
@api.multi
|
|
def action_rfq_send(self):
|
|
'''
|
|
This function opens a window to compose an email, with the edi purchase template message loaded by default
|
|
'''
|
|
self.ensure_one()
|
|
ir_model_data = self.env['ir.model.data']
|
|
try:
|
|
if self.env.context.get('send_rfq', False):
|
|
template_id = ir_model_data.get_object_reference('purchase', 'email_template_edi_purchase')[1]
|
|
else:
|
|
template_id = ir_model_data.get_object_reference('purchase', 'email_template_edi_purchase_done')[1]
|
|
except ValueError:
|
|
template_id = False
|
|
try:
|
|
compose_form_id = ir_model_data.get_object_reference('mail', 'email_compose_message_wizard_form')[1]
|
|
except ValueError:
|
|
compose_form_id = False
|
|
ctx = dict(self.env.context or {})
|
|
ctx.update({
|
|
'default_model': 'purchase.order',
|
|
'default_res_id': self.ids[0],
|
|
'default_use_template': bool(template_id),
|
|
'default_template_id': template_id,
|
|
'default_composition_mode': 'comment',
|
|
})
|
|
return {
|
|
'name': _('Compose Email'),
|
|
'type': 'ir.actions.act_window',
|
|
'view_type': 'form',
|
|
'view_mode': 'form',
|
|
'res_model': 'mail.compose.message',
|
|
'views': [(compose_form_id, 'form')],
|
|
'view_id': compose_form_id,
|
|
'target': 'new',
|
|
'context': ctx,
|
|
}
|
|
|
|
@api.multi
|
|
def print_quotation(self):
|
|
self.write({'state': "sent"})
|
|
return self.env['report'].get_action(self, 'purchase.report_purchasequotation')
|
|
|
|
@api.multi
|
|
def button_approve(self, force=False):
|
|
self.write({'state': 'purchase', 'date_approve': fields.Date.context_today(self)})
|
|
self._create_picking()
|
|
self.filtered(
|
|
lambda p: p.company_id.po_lock == 'lock').write({'state': 'done'})
|
|
return {}
|
|
|
|
@api.multi
|
|
def button_draft(self):
|
|
self.write({'state': 'draft'})
|
|
return {}
|
|
|
|
@api.multi
|
|
def button_confirm(self):
|
|
for order in self:
|
|
if order.state not in ['draft', 'sent']:
|
|
continue
|
|
order._add_supplier_to_product()
|
|
# Deal with double validation process
|
|
if order.company_id.po_double_validation == 'one_step'\
|
|
or (order.company_id.po_double_validation == 'two_step'\
|
|
and order.amount_total < self.env.user.company_id.currency_id.compute(order.company_id.po_double_validation_amount, order.currency_id))\
|
|
or order.user_has_groups('purchase.group_purchase_manager'):
|
|
order.button_approve()
|
|
else:
|
|
order.write({'state': 'to approve'})
|
|
return True
|
|
|
|
@api.multi
|
|
def button_cancel(self):
|
|
for order in self:
|
|
for pick in order.picking_ids:
|
|
if pick.state == 'done':
|
|
raise UserError(_('Unable to cancel purchase order %s as some receptions have already been done.') % (order.name))
|
|
for inv in order.invoice_ids:
|
|
if inv and inv.state not in ('cancel', 'draft'):
|
|
raise UserError(_("Unable to cancel this purchase order. You must first cancel related vendor bills."))
|
|
|
|
for pick in order.picking_ids.filtered(lambda r: r.state != 'cancel'):
|
|
pick.action_cancel()
|
|
# TDE FIXME: I don' think context key is necessary, as actions are not related / called from each other
|
|
if not self.env.context.get('cancel_procurement'):
|
|
procurements = order.order_line.mapped('procurement_ids')
|
|
procurements.filtered(lambda r: r.state not in ('cancel', 'exception') and r.rule_id.propagate).write({'state': 'cancel'})
|
|
procurements.filtered(lambda r: r.state not in ('cancel', 'exception') and not r.rule_id.propagate).write({'state': 'exception'})
|
|
moves = procurements.filtered(lambda r: r.rule_id.propagate).mapped('move_dest_id')
|
|
moves.filtered(lambda r: r.state != 'cancel').action_cancel()
|
|
|
|
self.write({'state': 'cancel'})
|
|
|
|
@api.multi
|
|
def button_unlock(self):
|
|
self.write({'state': 'purchase'})
|
|
|
|
@api.multi
|
|
def button_done(self):
|
|
self.write({'state': 'done'})
|
|
|
|
@api.multi
|
|
def _get_destination_location(self):
|
|
self.ensure_one()
|
|
if self.dest_address_id:
|
|
return self.dest_address_id.property_stock_customer.id
|
|
return self.picking_type_id.default_location_dest_id.id
|
|
|
|
@api.model
|
|
def _prepare_picking(self):
|
|
if not self.group_id:
|
|
self.group_id = self.group_id.create({
|
|
'name': self.name,
|
|
'partner_id': self.partner_id.id
|
|
})
|
|
if not self.partner_id.property_stock_supplier.id:
|
|
raise UserError(_("You must set a Vendor Location for this partner %s") % self.partner_id.name)
|
|
return {
|
|
'picking_type_id': self.picking_type_id.id,
|
|
'partner_id': self.partner_id.id,
|
|
'date': self.date_order,
|
|
'origin': self.name,
|
|
'location_dest_id': self._get_destination_location(),
|
|
'location_id': self.partner_id.property_stock_supplier.id,
|
|
'company_id': self.company_id.id,
|
|
}
|
|
|
|
@api.multi
|
|
def _create_picking(self):
|
|
StockPicking = self.env['stock.picking']
|
|
for order in self:
|
|
if any([ptype in ['product', 'consu'] for ptype in order.order_line.mapped('product_id.type')]):
|
|
pickings = order.picking_ids.filtered(lambda x: x.state not in ('done','cancel'))
|
|
if not pickings:
|
|
res = order._prepare_picking()
|
|
picking = StockPicking.create(res)
|
|
else:
|
|
picking = pickings[0]
|
|
moves = order.order_line._create_stock_moves(picking)
|
|
moves = moves.filtered(lambda x: x.state not in ('done', 'cancel')).action_confirm()
|
|
seq = 0
|
|
for move in sorted(moves, key=lambda move: move.date_expected):
|
|
seq += 5
|
|
move.sequence = seq
|
|
moves.force_assign()
|
|
picking.message_post_with_view('mail.message_origin_link',
|
|
values={'self': picking, 'origin': order},
|
|
subtype_id=self.env.ref('mail.mt_note').id)
|
|
return True
|
|
|
|
@api.multi
|
|
def _add_supplier_to_product(self):
|
|
# Add the partner in the supplier list of the product if the supplier is not registered for
|
|
# this product. We limit to 10 the number of suppliers for a product to avoid the mess that
|
|
# could be caused for some generic products ("Miscellaneous").
|
|
for line in self.order_line:
|
|
# Do not add a contact as a supplier
|
|
partner = self.partner_id if not self.partner_id.parent_id else self.partner_id.parent_id
|
|
if partner not in line.product_id.seller_ids.mapped('name') and len(line.product_id.seller_ids) <= 10:
|
|
currency = partner.property_purchase_currency_id or self.env.user.company_id.currency_id
|
|
supplierinfo = {
|
|
'name': partner.id,
|
|
'sequence': max(line.product_id.seller_ids.mapped('sequence')) + 1 if line.product_id.seller_ids else 1,
|
|
'product_uom': line.product_uom.id,
|
|
'min_qty': 0.0,
|
|
'price': self.currency_id.compute(line.price_unit, currency),
|
|
'currency_id': currency.id,
|
|
'delay': 0,
|
|
}
|
|
vals = {
|
|
'seller_ids': [(0, 0, supplierinfo)],
|
|
}
|
|
try:
|
|
line.product_id.write(vals)
|
|
except AccessError: # no write access rights -> just ignore
|
|
break
|
|
|
|
@api.multi
|
|
def action_view_picking(self):
|
|
'''
|
|
This function returns an action that display existing picking orders of given purchase order ids.
|
|
When only one found, show the picking immediately.
|
|
'''
|
|
action = self.env.ref('stock.action_picking_tree')
|
|
result = action.read()[0]
|
|
|
|
#override the context to get rid of the default filtering on picking type
|
|
result.pop('id', None)
|
|
result['context'] = {}
|
|
pick_ids = sum([order.picking_ids.ids for order in self], [])
|
|
#choose the view_mode accordingly
|
|
if len(pick_ids) > 1:
|
|
result['domain'] = "[('id','in',[" + ','.join(map(str, pick_ids)) + "])]"
|
|
elif len(pick_ids) == 1:
|
|
res = self.env.ref('stock.view_picking_form', False)
|
|
result['views'] = [(res and res.id or False, 'form')]
|
|
result['res_id'] = pick_ids and pick_ids[0] or False
|
|
return result
|
|
|
|
@api.multi
|
|
def action_view_invoice(self):
|
|
'''
|
|
This function returns an action that display existing vendor bills of given purchase order ids.
|
|
When only one found, show the vendor bill immediately.
|
|
'''
|
|
action = self.env.ref('account.action_invoice_tree2')
|
|
result = action.read()[0]
|
|
|
|
#override the context to get rid of the default filtering
|
|
result['context'] = {'type': 'in_invoice', 'default_purchase_id': self.id}
|
|
|
|
if not self.invoice_ids:
|
|
# Choose a default account journal in the same currency in case a new invoice is created
|
|
journal_domain = [
|
|
('type', '=', 'purchase'),
|
|
('company_id', '=', self.company_id.id),
|
|
('currency_id', '=', self.currency_id.id),
|
|
]
|
|
default_journal_id = self.env['account.journal'].search(journal_domain, limit=1)
|
|
if default_journal_id:
|
|
result['context']['default_journal_id'] = default_journal_id.id
|
|
else:
|
|
# Use the same account journal than a previous invoice
|
|
result['context']['default_journal_id'] = self.invoice_ids[0].journal_id.id
|
|
|
|
#choose the view_mode accordingly
|
|
if len(self.invoice_ids) != 1:
|
|
result['domain'] = "[('id', 'in', " + str(self.invoice_ids.ids) + ")]"
|
|
elif len(self.invoice_ids) == 1:
|
|
res = self.env.ref('account.invoice_supplier_form', False)
|
|
result['views'] = [(res and res.id or False, 'form')]
|
|
result['res_id'] = self.invoice_ids.id
|
|
return result
|
|
|
|
@api.multi
|
|
def action_set_date_planned(self):
|
|
for order in self:
|
|
order.order_line.update({'date_planned': order.date_planned})
|
|
|
|
|
|
class PurchaseOrderLine(models.Model):
|
|
_name = 'purchase.order.line'
|
|
_description = 'Purchase Order Line'
|
|
_order = 'sequence, id'
|
|
|
|
@api.depends('product_qty', 'price_unit', 'taxes_id')
|
|
def _compute_amount(self):
|
|
for line in self:
|
|
taxes = line.taxes_id.compute_all(line.price_unit, line.order_id.currency_id, line.product_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'],
|
|
})
|
|
|
|
@api.multi
|
|
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 company_id is set, always filter taxes by the company
|
|
taxes = line.product_id.supplier_taxes_id.filtered(lambda r: not line.company_id or r.company_id == line.company_id)
|
|
line.taxes_id = fpos.map_tax(taxes, line.product_id, line.order_id.partner_id) if fpos else taxes
|
|
|
|
@api.depends('invoice_lines.invoice_id.state')
|
|
def _compute_qty_invoiced(self):
|
|
for line in self:
|
|
qty = 0.0
|
|
for inv_line in line.invoice_lines:
|
|
if inv_line.invoice_id.state not in ['cancel']:
|
|
if inv_line.invoice_id.type == 'in_invoice':
|
|
qty += inv_line.uom_id._compute_quantity(inv_line.quantity, line.product_uom)
|
|
elif inv_line.invoice_id.type == 'in_refund':
|
|
qty -= inv_line.uom_id._compute_quantity(inv_line.quantity, line.product_uom)
|
|
line.qty_invoiced = qty
|
|
|
|
@api.depends('order_id.state', 'move_ids.state')
|
|
def _compute_qty_received(self):
|
|
for line in self:
|
|
if line.order_id.state not in ['purchase', 'done']:
|
|
line.qty_received = 0.0
|
|
continue
|
|
if line.product_id.type not in ['consu', 'product']:
|
|
line.qty_received = line.product_qty
|
|
continue
|
|
total = 0.0
|
|
for move in line.move_ids:
|
|
if move.state == 'done':
|
|
if move.product_uom != line.product_uom:
|
|
total += move.product_uom._compute_quantity(move.product_uom_qty, line.product_uom)
|
|
else:
|
|
total += move.product_uom_qty
|
|
line.qty_received = total
|
|
|
|
@api.model
|
|
def create(self, values):
|
|
line = super(PurchaseOrderLine, self).create(values)
|
|
if line.order_id.state == 'purchase':
|
|
line.order_id._create_picking()
|
|
msg = _("Extra line with %s ") % (line.product_id.display_name,)
|
|
line.order_id.message_post(body=msg)
|
|
return line
|
|
|
|
@api.multi
|
|
def write(self, values):
|
|
orders = False
|
|
if 'product_qty' in values:
|
|
changed_lines = self.filtered(lambda x: x.order_id.state == 'purchase')
|
|
if changed_lines:
|
|
orders = changed_lines.mapped('order_id')
|
|
for order in orders:
|
|
order_lines = changed_lines.filtered(lambda x: x.order_id == order)
|
|
msg = ""
|
|
if any([values['product_qty'] < x.product_qty for x in order_lines]):
|
|
msg += "<b>" + _('The ordered quantity has been decreased. Do not forget to take it into account on your bills and receipts.') + '</b><br/>'
|
|
msg += "<ul>"
|
|
for line in order_lines:
|
|
msg += "<li> %s:" % (line.product_id.display_name,)
|
|
msg += "<br/>" + _("Ordered Quantity") + ": %s -> %s <br/>" % (line.product_qty, float(values['product_qty']),)
|
|
if line.product_id.type in ('product', 'consu'):
|
|
msg += _("Received Quantity") + ": %s <br/>" % (line.qty_received,)
|
|
msg += _("Billed Quantity") + ": %s <br/></li>" % (line.qty_invoiced,)
|
|
msg += "</ul>"
|
|
order.message_post(body=msg)
|
|
# Update expectged date of corresponding moves
|
|
if 'date_planned' in values:
|
|
self.env['stock.move'].search([
|
|
('purchase_line_id', 'in', self.ids), ('state', '!=', 'done')
|
|
]).write({'date_expected': values['date_planned']})
|
|
result = super(PurchaseOrderLine, self).write(values)
|
|
if orders:
|
|
orders._create_picking()
|
|
return result
|
|
|
|
name = fields.Text(string='Description', required=True)
|
|
sequence = fields.Integer(string='Sequence', default=10)
|
|
product_qty = fields.Float(string='Quantity', digits=dp.get_precision('Product Unit of Measure'), required=True)
|
|
date_planned = fields.Datetime(string='Scheduled Date', required=True, index=True)
|
|
taxes_id = fields.Many2many('account.tax', string='Taxes', domain=['|', ('active', '=', False), ('active', '=', True)])
|
|
product_uom = fields.Many2one('product.uom', string='Product Unit of Measure', required=True)
|
|
product_id = fields.Many2one('product.product', string='Product', domain=[('purchase_ok', '=', True)], change_default=True, required=True)
|
|
move_ids = fields.One2many('stock.move', 'purchase_line_id', string='Reservation', readonly=True, ondelete='set null', copy=False)
|
|
price_unit = fields.Float(string='Unit Price', required=True, digits=dp.get_precision('Product Price'))
|
|
|
|
price_subtotal = fields.Monetary(compute='_compute_amount', string='Subtotal', store=True)
|
|
price_total = fields.Monetary(compute='_compute_amount', string='Total', store=True)
|
|
price_tax = fields.Monetary(compute='_compute_amount', string='Tax', store=True)
|
|
|
|
order_id = fields.Many2one('purchase.order', string='Order Reference', index=True, required=True, ondelete='cascade')
|
|
account_analytic_id = fields.Many2one('account.analytic.account', string='Analytic Account')
|
|
analytic_tag_ids = fields.Many2many('account.analytic.tag', string='Analytic Tags')
|
|
company_id = fields.Many2one('res.company', related='order_id.company_id', string='Company', store=True, readonly=True)
|
|
state = fields.Selection(related='order_id.state', store=True)
|
|
|
|
invoice_lines = fields.One2many('account.invoice.line', 'purchase_line_id', string="Bill Lines", readonly=True, copy=False)
|
|
|
|
# Replace by invoiced Qty
|
|
qty_invoiced = fields.Float(compute='_compute_qty_invoiced', string="Billed Qty", digits=dp.get_precision('Product Unit of Measure'), store=True)
|
|
qty_received = fields.Float(compute='_compute_qty_received', string="Received Qty", digits=dp.get_precision('Product Unit of Measure'), store=True)
|
|
|
|
partner_id = fields.Many2one('res.partner', related='order_id.partner_id', string='Partner', readonly=True, store=True)
|
|
currency_id = fields.Many2one(related='order_id.currency_id', store=True, string='Currency', readonly=True)
|
|
date_order = fields.Datetime(related='order_id.date_order', string='Order Date', readonly=True)
|
|
procurement_ids = fields.One2many('procurement.order', 'purchase_line_id', string='Associated Procurements', copy=False)
|
|
|
|
@api.multi
|
|
def _get_stock_move_price_unit(self):
|
|
self.ensure_one()
|
|
line = self[0]
|
|
order = line.order_id
|
|
price_unit = line.price_unit
|
|
if line.taxes_id:
|
|
price_unit = line.taxes_id.with_context(round=False).compute_all(
|
|
price_unit, currency=line.order_id.currency_id, quantity=1.0, product=line.product_id, partner=line.order_id.partner_id
|
|
)['total_excluded']
|
|
if line.product_uom.id != line.product_id.uom_id.id:
|
|
price_unit *= line.product_uom.factor / line.product_id.uom_id.factor
|
|
if order.currency_id != order.company_id.currency_id:
|
|
price_unit = order.currency_id.compute(price_unit, order.company_id.currency_id, round=False)
|
|
return price_unit
|
|
|
|
@api.multi
|
|
def _prepare_stock_moves(self, picking):
|
|
""" Prepare the stock moves data for one order line. This function returns a list of
|
|
dictionary ready to be used in stock.move's create()
|
|
"""
|
|
self.ensure_one()
|
|
res = []
|
|
if self.product_id.type not in ['product', 'consu']:
|
|
return res
|
|
qty = 0.0
|
|
price_unit = self._get_stock_move_price_unit()
|
|
for move in self.move_ids.filtered(lambda x: x.state != 'cancel'):
|
|
qty += move.product_qty
|
|
template = {
|
|
'name': self.name or '',
|
|
'product_id': self.product_id.id,
|
|
'product_uom': self.product_uom.id,
|
|
'date': self.order_id.date_order,
|
|
'date_expected': self.date_planned,
|
|
'location_id': self.order_id.partner_id.property_stock_supplier.id,
|
|
'location_dest_id': self.order_id._get_destination_location(),
|
|
'picking_id': picking.id,
|
|
'partner_id': self.order_id.dest_address_id.id,
|
|
'move_dest_id': False,
|
|
'state': 'draft',
|
|
'purchase_line_id': self.id,
|
|
'company_id': self.order_id.company_id.id,
|
|
'price_unit': price_unit,
|
|
'picking_type_id': self.order_id.picking_type_id.id,
|
|
'group_id': self.order_id.group_id.id,
|
|
'procurement_id': False,
|
|
'origin': self.order_id.name,
|
|
'route_ids': self.order_id.picking_type_id.warehouse_id and [(6, 0, [x.id for x in self.order_id.picking_type_id.warehouse_id.route_ids])] or [],
|
|
'warehouse_id': self.order_id.picking_type_id.warehouse_id.id,
|
|
}
|
|
# Fullfill all related procurements with this po line
|
|
diff_quantity = self.product_qty - qty
|
|
for procurement in self.procurement_ids.filtered(lambda p: p.state != 'cancel'):
|
|
# If the procurement has some moves already, we should deduct their quantity
|
|
sum_existing_moves = sum(x.product_qty for x in procurement.move_ids if x.state != 'cancel')
|
|
existing_proc_qty = procurement.product_id.uom_id._compute_quantity(sum_existing_moves, procurement.product_uom)
|
|
procurement_qty = procurement.product_uom._compute_quantity(procurement.product_qty, self.product_uom) - existing_proc_qty
|
|
if float_compare(procurement_qty, 0.0, precision_rounding=procurement.product_uom.rounding) > 0 and float_compare(diff_quantity, 0.0, precision_rounding=self.product_uom.rounding) > 0:
|
|
tmp = template.copy()
|
|
tmp.update({
|
|
'product_uom_qty': min(procurement_qty, diff_quantity),
|
|
'move_dest_id': procurement.move_dest_id.id, # move destination is same as procurement destination
|
|
'procurement_id': procurement.id,
|
|
'propagate': procurement.rule_id.propagate,
|
|
})
|
|
res.append(tmp)
|
|
diff_quantity -= min(procurement_qty, diff_quantity)
|
|
if float_compare(diff_quantity, 0.0, precision_rounding=self.product_uom.rounding) > 0:
|
|
template['product_uom_qty'] = diff_quantity
|
|
res.append(template)
|
|
return res
|
|
|
|
@api.multi
|
|
def _create_stock_moves(self, picking):
|
|
moves = self.env['stock.move']
|
|
done = self.env['stock.move'].browse()
|
|
for line in self:
|
|
for val in line._prepare_stock_moves(picking):
|
|
done += moves.create(val)
|
|
return done
|
|
|
|
@api.multi
|
|
def unlink(self):
|
|
for line in self:
|
|
if line.order_id.state in ['purchase', 'done']:
|
|
raise UserError(_('Cannot delete a purchase order line which is in state \'%s\'.') %(line.state,))
|
|
for proc in line.procurement_ids:
|
|
proc.message_post(body=_('Purchase order line deleted.'))
|
|
line.procurement_ids.filtered(lambda r: r.state != 'cancel').write({'state': 'exception'})
|
|
return super(PurchaseOrderLine, self).unlink()
|
|
|
|
@api.model
|
|
def _get_date_planned(self, seller, po=False):
|
|
"""Return the datetime value to use as Schedule Date (``date_planned``) for
|
|
PO Lines that correspond to the given product.seller_ids,
|
|
when ordered at `date_order_str`.
|
|
|
|
:param browse_record | False product: product.product, used to
|
|
determine delivery delay thanks to the selected seller field (if False, default delay = 0)
|
|
:param browse_record | False po: purchase.order, necessary only if
|
|
the PO line is not yet attached to a PO.
|
|
:rtype: datetime
|
|
:return: desired Schedule Date for the PO line
|
|
"""
|
|
date_order = po.date_order if po else self.order_id.date_order
|
|
if date_order:
|
|
return datetime.strptime(date_order, DEFAULT_SERVER_DATETIME_FORMAT) + relativedelta(days=seller.delay if seller else 0)
|
|
else:
|
|
return datetime.today() + relativedelta(days=seller.delay if seller else 0)
|
|
|
|
@api.onchange('product_id')
|
|
def onchange_product_id(self):
|
|
result = {}
|
|
if not self.product_id:
|
|
return result
|
|
|
|
# Reset date, price and quantity since _onchange_quantity will provide default values
|
|
self.date_planned = datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT)
|
|
self.price_unit = self.product_qty = 0.0
|
|
self.product_uom = self.product_id.uom_po_id or self.product_id.uom_id
|
|
result['domain'] = {'product_uom': [('category_id', '=', self.product_id.uom_id.category_id.id)]}
|
|
|
|
product_lang = self.product_id.with_context(
|
|
lang=self.partner_id.lang,
|
|
partner_id=self.partner_id.id,
|
|
)
|
|
self.name = product_lang.display_name
|
|
if product_lang.description_purchase:
|
|
self.name += '\n' + product_lang.description_purchase
|
|
|
|
fpos = self.order_id.fiscal_position_id
|
|
if self.env.uid == SUPERUSER_ID:
|
|
company_id = self.env.user.company_id.id
|
|
self.taxes_id = fpos.map_tax(self.product_id.supplier_taxes_id.filtered(lambda r: r.company_id.id == company_id))
|
|
else:
|
|
self.taxes_id = fpos.map_tax(self.product_id.supplier_taxes_id)
|
|
|
|
self._suggest_quantity()
|
|
self._onchange_quantity()
|
|
|
|
return result
|
|
|
|
@api.onchange('product_id')
|
|
def onchange_product_id_warning(self):
|
|
if not self.product_id:
|
|
return
|
|
warning = {}
|
|
title = False
|
|
message = False
|
|
|
|
product_info = self.product_id
|
|
|
|
if product_info.purchase_line_warn != 'no-message':
|
|
title = _("Warning for %s") % product_info.name
|
|
message = product_info.purchase_line_warn_msg
|
|
warning['title'] = title
|
|
warning['message'] = message
|
|
if product_info.purchase_line_warn == 'block':
|
|
self.product_id = False
|
|
return {'warning': warning}
|
|
return {}
|
|
|
|
@api.onchange('product_qty', 'product_uom')
|
|
def _onchange_quantity(self):
|
|
if not self.product_id:
|
|
return
|
|
|
|
seller = self.product_id._select_seller(
|
|
partner_id=self.partner_id,
|
|
quantity=self.product_qty,
|
|
date=self.order_id.date_order and self.order_id.date_order[:10],
|
|
uom_id=self.product_uom)
|
|
|
|
if seller or not self.date_planned:
|
|
self.date_planned = self._get_date_planned(seller).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
|
|
|
|
if not seller:
|
|
return
|
|
|
|
price_unit = self.env['account.tax']._fix_tax_included_price_company(seller.price, self.product_id.supplier_taxes_id, self.taxes_id, self.company_id) if seller else 0.0
|
|
if price_unit and seller and self.order_id.currency_id and seller.currency_id != self.order_id.currency_id:
|
|
price_unit = seller.currency_id.compute(price_unit, self.order_id.currency_id)
|
|
|
|
if seller and self.product_uom and seller.product_uom != self.product_uom:
|
|
price_unit = seller.product_uom._compute_price(price_unit, self.product_uom)
|
|
|
|
self.price_unit = price_unit
|
|
|
|
@api.onchange('product_qty')
|
|
def _onchange_product_qty(self):
|
|
if (self.state == 'purchase' or self.state == 'to approve') and self.product_id.type in ['product', 'consu'] and self.product_qty < self._origin.product_qty:
|
|
warning_mess = {
|
|
'title': _('Ordered quantity decreased!'),
|
|
'message' : _('You are decreasing the ordered quantity!\nYou must update the quantities on the reception and/or bills.'),
|
|
}
|
|
return {'warning': warning_mess}
|
|
|
|
def _suggest_quantity(self):
|
|
'''
|
|
Suggest a minimal quantity based on the seller
|
|
'''
|
|
if not self.product_id:
|
|
return
|
|
|
|
seller_min_qty = self.product_id.seller_ids\
|
|
.filtered(lambda r: r.name == self.order_id.partner_id)\
|
|
.sorted(key=lambda r: r.min_qty)
|
|
if seller_min_qty:
|
|
self.product_qty = seller_min_qty[0].min_qty or 1.0
|
|
self.product_uom = seller_min_qty[0].product_uom
|
|
else:
|
|
self.product_qty = 1.0
|
|
|
|
|
|
class ProcurementRule(models.Model):
|
|
_inherit = 'procurement.rule'
|
|
|
|
@api.model
|
|
def _get_action(self):
|
|
return [('buy', _('Buy'))] + super(ProcurementRule, self)._get_action()
|
|
|
|
|
|
class ProcurementOrder(models.Model):
|
|
_inherit = 'procurement.order'
|
|
|
|
purchase_line_id = fields.Many2one('purchase.order.line', string='Purchase Order Line')
|
|
purchase_id = fields.Many2one(related='purchase_line_id.order_id', string='Purchase Order')
|
|
|
|
@api.multi
|
|
def propagate_cancels(self):
|
|
result = super(ProcurementOrder, self).propagate_cancels()
|
|
for procurement in self:
|
|
if procurement.rule_id.action == 'buy' and procurement.purchase_line_id:
|
|
if procurement.purchase_line_id.order_id.state not in ('draft', 'cancel', 'sent', 'to validate'):
|
|
raise UserError(
|
|
_('Can not cancel a procurement related to a purchase order. Please cancel the purchase order first.'))
|
|
if procurement.purchase_line_id:
|
|
price_unit = 0.0
|
|
product_qty = 0.0
|
|
others_procs = procurement.purchase_line_id.procurement_ids.filtered(lambda r: r != procurement)
|
|
for other_proc in others_procs:
|
|
if other_proc.state not in ['cancel', 'draft']:
|
|
product_qty += other_proc.product_uom._compute_quantity(other_proc.product_qty, procurement.purchase_line_id.product_uom)
|
|
|
|
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
|
if not float_is_zero(product_qty, precision_digits=precision):
|
|
seller = procurement.product_id._select_seller(
|
|
partner_id=procurement.purchase_line_id.partner_id,
|
|
quantity=product_qty,
|
|
date=procurement.purchase_line_id.order_id.date_order and procurement.purchase_line_id.order_id.date_order[:10],
|
|
uom_id=procurement.purchase_line_id.product_uom)
|
|
|
|
price_unit = self.env['account.tax']._fix_tax_included_price_company(seller.price, procurement.purchase_line_id.product_id.supplier_taxes_id, procurement.purchase_line_id.taxes_id, procurement.company_id) if seller else 0.0
|
|
if price_unit and seller and procurement.purchase_line_id.order_id.currency_id and seller.currency_id != procurement.purchase_line_id.order_id.currency_id:
|
|
price_unit = seller.currency_id.compute(price_unit, procurement.purchase_line_id.order_id.currency_id)
|
|
|
|
if seller and seller.product_uom != procurement.purchase_line_id.product_uom:
|
|
price_unit = seller.product_uom._compute_price(price_unit, procurement.purchase_line_id.product_uom)
|
|
|
|
procurement.purchase_line_id.product_qty = product_qty
|
|
procurement.purchase_line_id.price_unit = price_unit
|
|
else:
|
|
procurement.purchase_line_id.unlink()
|
|
|
|
return result
|
|
|
|
@api.multi
|
|
def _run(self):
|
|
if self.rule_id and self.rule_id.action == 'buy':
|
|
return self.make_po()
|
|
return super(ProcurementOrder, self)._run()
|
|
|
|
@api.multi
|
|
def _check(self):
|
|
if self.purchase_line_id:
|
|
if not self.move_ids:
|
|
return False
|
|
return all(move.state in ('done', 'cancel') for move in self.move_ids) and any(move.state == 'done' for move in self.move_ids)
|
|
return super(ProcurementOrder, self)._check()
|
|
|
|
def _get_purchase_schedule_date(self):
|
|
"""Return the datetime value to use as Schedule Date (``date_planned``) for the
|
|
Purchase Order Lines created to satisfy the given procurement. """
|
|
procurement_date_planned = datetime.strptime(self.date_planned, DEFAULT_SERVER_DATETIME_FORMAT)
|
|
schedule_date = (procurement_date_planned - relativedelta(days=self.company_id.po_lead))
|
|
return schedule_date
|
|
|
|
def _get_purchase_order_date(self, schedule_date):
|
|
"""Return the datetime value to use as Order Date (``date_order``) for the
|
|
Purchase Order created to satisfy the given procurement. """
|
|
self.ensure_one()
|
|
seller_delay = int(self.product_id._select_seller(quantity=self.product_qty, uom_id=self.product_uom).delay)
|
|
return schedule_date - relativedelta(days=seller_delay)
|
|
|
|
@api.multi
|
|
def _prepare_purchase_order_line(self, po, supplier):
|
|
self.ensure_one()
|
|
|
|
procurement_uom_po_qty = self.product_uom._compute_quantity(self.product_qty, self.product_id.uom_po_id)
|
|
seller = self.product_id._select_seller(
|
|
partner_id=supplier.name,
|
|
quantity=procurement_uom_po_qty,
|
|
date=po.date_order and po.date_order[:10],
|
|
uom_id=self.product_id.uom_po_id)
|
|
|
|
taxes = self.product_id.supplier_taxes_id
|
|
fpos = po.fiscal_position_id
|
|
taxes_id = fpos.map_tax(taxes) if fpos else taxes
|
|
if taxes_id:
|
|
taxes_id = taxes_id.filtered(lambda x: x.company_id.id == self.company_id.id)
|
|
|
|
price_unit = self.env['account.tax']._fix_tax_included_price_company(seller.price, self.product_id.supplier_taxes_id, taxes_id, self.company_id) if seller else 0.0
|
|
if price_unit and seller and po.currency_id and seller.currency_id != po.currency_id:
|
|
price_unit = seller.currency_id.compute(price_unit, po.currency_id)
|
|
|
|
product_lang = self.product_id.with_context({
|
|
'lang': supplier.name.lang,
|
|
'partner_id': supplier.name.id,
|
|
})
|
|
name = product_lang.display_name
|
|
if product_lang.description_purchase:
|
|
name += '\n' + product_lang.description_purchase
|
|
|
|
date_planned = self.env['purchase.order.line']._get_date_planned(seller, po=po).strftime(DEFAULT_SERVER_DATETIME_FORMAT)
|
|
|
|
return {
|
|
'name': name,
|
|
'product_qty': procurement_uom_po_qty,
|
|
'product_id': self.product_id.id,
|
|
'product_uom': self.product_id.uom_po_id.id,
|
|
'price_unit': price_unit,
|
|
'date_planned': date_planned,
|
|
'taxes_id': [(6, 0, taxes_id.ids)],
|
|
'procurement_ids': [(4, self.id)],
|
|
'order_id': po.id,
|
|
}
|
|
|
|
@api.multi
|
|
def _prepare_purchase_order(self, partner):
|
|
self.ensure_one()
|
|
schedule_date = self._get_purchase_schedule_date()
|
|
purchase_date = self._get_purchase_order_date(schedule_date)
|
|
fpos = self.env['account.fiscal.position'].with_context(force_company=self.company_id.id).get_fiscal_position(partner.id)
|
|
|
|
gpo = self.rule_id.group_propagation_option
|
|
group = (gpo == 'fixed' and self.rule_id.group_id.id) or \
|
|
(gpo == 'propagate' and self.group_id.id) or False
|
|
|
|
return {
|
|
'partner_id': partner.id,
|
|
'picking_type_id': self.rule_id.picking_type_id.id,
|
|
'company_id': self.company_id.id,
|
|
'currency_id': partner.property_purchase_currency_id.id or self.env.user.company_id.currency_id.id,
|
|
'dest_address_id': self.partner_dest_id.id,
|
|
'origin': self.origin,
|
|
'payment_term_id': partner.property_supplier_payment_term_id.id,
|
|
'date_order': purchase_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
|
|
'fiscal_position_id': fpos,
|
|
'group_id': group
|
|
}
|
|
|
|
def _make_po_select_supplier(self, suppliers):
|
|
""" Method intended to be overridden by customized modules to implement any logic in the
|
|
selection of supplier.
|
|
"""
|
|
return suppliers[0]
|
|
|
|
def _make_po_get_domain(self, partner):
|
|
gpo = self.rule_id.group_propagation_option
|
|
group = (gpo == 'fixed' and self.rule_id.group_id) or \
|
|
(gpo == 'propagate' and self.group_id) or False
|
|
|
|
domain = (
|
|
('partner_id', '=', partner.id),
|
|
('state', '=', 'draft'),
|
|
('picking_type_id', '=', self.rule_id.picking_type_id.id),
|
|
('company_id', '=', self.company_id.id),
|
|
('dest_address_id', '=', self.partner_dest_id.id))
|
|
if group:
|
|
domain += (('group_id', '=', group.id),)
|
|
return domain
|
|
|
|
@api.multi
|
|
def make_po(self):
|
|
cache = {}
|
|
res = []
|
|
for procurement in self:
|
|
suppliers = procurement.product_id.seller_ids\
|
|
.filtered(lambda r: (not r.company_id or r.company_id == procurement.company_id) and (not r.product_id or r.product_id == procurement.product_id))
|
|
if not suppliers:
|
|
procurement.message_post(body=_('No vendor associated to product %s. Please set one to fix this procurement.') % (procurement.product_id.name))
|
|
continue
|
|
supplier = procurement._make_po_select_supplier(suppliers)
|
|
partner = supplier.name
|
|
|
|
domain = procurement._make_po_get_domain(partner)
|
|
|
|
if domain in cache:
|
|
po = cache[domain]
|
|
else:
|
|
po = self.env['purchase.order'].search([dom for dom in domain])
|
|
po = po[0] if po else False
|
|
cache[domain] = po
|
|
if not po:
|
|
vals = procurement._prepare_purchase_order(partner)
|
|
po = self.env['purchase.order'].create(vals)
|
|
name = (procurement.group_id and (procurement.group_id.name + ":") or "") + (procurement.name != "/" and procurement.name or procurement.move_dest_id.raw_material_production_id and procurement.move_dest_id.raw_material_production_id.name or "")
|
|
message = _("This purchase order has been created from: <a href=# data-oe-model=procurement.order data-oe-id=%d>%s</a>") % (procurement.id, name)
|
|
po.message_post(body=message)
|
|
cache[domain] = po
|
|
elif not po.origin or procurement.origin not in po.origin.split(', '):
|
|
# Keep track of all procurements
|
|
if po.origin:
|
|
if procurement.origin:
|
|
po.write({'origin': po.origin + ', ' + procurement.origin})
|
|
else:
|
|
po.write({'origin': po.origin})
|
|
else:
|
|
po.write({'origin': procurement.origin})
|
|
name = (self.group_id and (self.group_id.name + ":") or "") + (self.name != "/" and self.name or self.move_dest_id.raw_material_production_id and self.move_dest_id.raw_material_production_id.name or "")
|
|
message = _("This purchase order has been modified from: <a href=# data-oe-model=procurement.order data-oe-id=%d>%s</a>") % (procurement.id, name)
|
|
po.message_post(body=message)
|
|
if po:
|
|
res += [procurement.id]
|
|
|
|
# Create Line
|
|
po_line = False
|
|
for line in po.order_line:
|
|
if line.product_id == procurement.product_id and line.product_uom == procurement.product_id.uom_po_id:
|
|
procurement_uom_po_qty = procurement.product_uom._compute_quantity(procurement.product_qty, procurement.product_id.uom_po_id)
|
|
seller = procurement.product_id._select_seller(
|
|
partner_id=partner,
|
|
quantity=line.product_qty + procurement_uom_po_qty,
|
|
date=po.date_order and po.date_order[:10],
|
|
uom_id=procurement.product_id.uom_po_id)
|
|
|
|
price_unit = self.env['account.tax']._fix_tax_included_price_company(seller.price, line.product_id.supplier_taxes_id, line.taxes_id, self.company_id) if seller else 0.0
|
|
if price_unit and seller and po.currency_id and seller.currency_id != po.currency_id:
|
|
price_unit = seller.currency_id.compute(price_unit, po.currency_id)
|
|
|
|
po_line = line.write({
|
|
'product_qty': line.product_qty + procurement_uom_po_qty,
|
|
'price_unit': price_unit,
|
|
'procurement_ids': [(4, procurement.id)]
|
|
})
|
|
break
|
|
if not po_line:
|
|
vals = procurement._prepare_purchase_order_line(po, supplier)
|
|
self.env['purchase.order.line'].create(vals)
|
|
return res
|
|
|
|
@api.multi
|
|
def open_purchase_order(self):
|
|
action = self.env.ref('purchase.purchase_order_action_generic')
|
|
action_dict = action.read()[0]
|
|
action_dict['res_id'] = self.purchase_id.id
|
|
action_dict['target'] = 'current'
|
|
return action_dict
|
|
|
|
|
|
class ProductTemplate(models.Model):
|
|
_name = 'product.template'
|
|
_inherit = 'product.template'
|
|
|
|
@api.model
|
|
def _get_buy_route(self):
|
|
buy_route = self.env.ref('purchase.route_warehouse0_buy', raise_if_not_found=False)
|
|
if buy_route:
|
|
return buy_route.ids
|
|
return []
|
|
|
|
@api.multi
|
|
def _purchase_count(self):
|
|
for template in self:
|
|
template.purchase_count = sum([p.purchase_count for p in template.product_variant_ids])
|
|
return True
|
|
|
|
property_account_creditor_price_difference = fields.Many2one(
|
|
'account.account', string="Price Difference Account", company_dependent=True,
|
|
help="This account will be used to value price difference between purchase price and cost price.")
|
|
purchase_count = fields.Integer(compute='_purchase_count', string='# Purchases')
|
|
purchase_method = fields.Selection([
|
|
('purchase', 'On ordered quantities'),
|
|
('receive', 'On received quantities'),
|
|
], string="Control Purchase Bills",
|
|
help="On ordered quantities: control bills based on ordered quantities.\n"
|
|
"On received quantities: control bills based on received quantity.", default="receive")
|
|
route_ids = fields.Many2many(default=lambda self: self._get_buy_route())
|
|
purchase_line_warn = fields.Selection(WARNING_MESSAGE, 'Purchase Order Line', help=WARNING_HELP, required=True, default="no-message")
|
|
purchase_line_warn_msg = fields.Text('Message for Purchase Order Line')
|
|
|
|
|
|
class ProductProduct(models.Model):
|
|
_name = 'product.product'
|
|
_inherit = 'product.product'
|
|
|
|
@api.multi
|
|
def _purchase_count(self):
|
|
domain = [
|
|
('state', 'in', ['purchase', 'done']),
|
|
('product_id', 'in', self.mapped('id')),
|
|
]
|
|
PurchaseOrderLines = self.env['purchase.order.line'].search(domain)
|
|
for product in self:
|
|
product.purchase_count = len(PurchaseOrderLines.filtered(lambda r: r.product_id == product).mapped('order_id'))
|
|
|
|
purchase_count = fields.Integer(compute='_purchase_count', string='# Purchases')
|
|
|
|
|
|
class ProductCategory(models.Model):
|
|
_inherit = "product.category"
|
|
|
|
property_account_creditor_price_difference_categ = fields.Many2one(
|
|
'account.account', string="Price Difference Account",
|
|
company_dependent=True,
|
|
help="This account will be used to value price difference between purchase price and accounting cost.")
|
|
|
|
|
|
class MailComposeMessage(models.TransientModel):
|
|
_inherit = 'mail.compose.message'
|
|
|
|
@api.multi
|
|
def mail_purchase_order_on_send(self):
|
|
if not self.filtered('subtype_id.internal'):
|
|
order = self.env['purchase.order'].browse(self._context['default_res_id'])
|
|
if order.state == 'draft':
|
|
order.state = 'sent'
|
|
|
|
@api.multi
|
|
def send_mail(self, auto_commit=False):
|
|
if self._context.get('default_model') == 'purchase.order' and self._context.get('default_res_id'):
|
|
self = self.with_context(mail_post_autofollow=True)
|
|
self.mail_purchase_order_on_send()
|
|
return super(MailComposeMessage, self).send_mail(auto_commit=auto_commit)
|