When deacreasing a quantity on a PO it will decrease the quantity of an associated move. When the move's quantity reach 0, previously the move was unlinked and aslo the picking if all moves inside were unlinked. However the destination move(s) still exist with the quantity that has been removed in the original move. It can confuse the end user which have a move without origin. This commit cancel the move/picking instead of unlink them. Thanks to this the end user still have the link with the original moves and can guess from where their destinations moves come from. It also remove the filter on PO in order to display the canceled picking.
1204 lines
58 KiB
Python
1204 lines
58 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
|
|
from odoo.addons import decimal_precision as dp
|
|
|
|
|
|
class PurchaseOrder(models.Model):
|
|
_name = "purchase.order"
|
|
_inherit = ['mail.thread', 'mail.activity.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
|
|
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):
|
|
order.invoice_status = 'invoiced'
|
|
else:
|
|
order.invoice_status = 'no'
|
|
|
|
@api.depends('order_line.invoice_lines.invoice_id')
|
|
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.returned_move_ids',
|
|
'order_line.move_ids.state',
|
|
'order_line.move_ids.picking_id')
|
|
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')
|
|
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 sales 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, store=True)
|
|
invoice_ids = fields.Many2many('account.invoice', compute="_compute_invoice", string='Bills', copy=False, store=True)
|
|
invoice_status = fields.Selection([
|
|
('no', 'Nothing to Bill'),
|
|
('to invoice', 'Waiting Bills'),
|
|
('invoiced', 'No Bill to Receive'),
|
|
], 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, store=True)
|
|
picking_ids = fields.Many2many('stock.picking', compute='_compute_picking', string='Receptions', copy=False, store=True)
|
|
|
|
# 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 operation 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',
|
|
'custom_layout': "purchase.mail_template_data_notification_email_purchase_order"
|
|
})
|
|
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):
|
|
return self.env.ref('purchase.report_purchase_quotation').report_action(self)
|
|
|
|
@api.multi
|
|
def button_approve(self, force=False):
|
|
self.write({'state': 'purchase'})
|
|
self._create_picking()
|
|
if self.company_id.po_lock == 'lock':
|
|
self.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.action_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 operation type
|
|
result['context'] = {}
|
|
pick_ids = self.mapped('picking_ids')
|
|
#choose the view_mode accordingly
|
|
if len(pick_ids) > 1:
|
|
result['domain'] = "[('id','in',%s)]" % (pick_ids.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.id
|
|
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 = 'order_id, 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': sum(t.get('amount', 0.0) for t in taxes.get('taxes', [])),
|
|
'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', 'invoice_lines.quantity')
|
|
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.location_dest_id.usage == "supplier":
|
|
if move.to_refund:
|
|
total -= move.product_uom._compute_quantity(move.product_uom_qty, line.product_uom)
|
|
else:
|
|
total += move.product_uom._compute_quantity(move.product_uom_qty, line.product_uom)
|
|
line.qty_received = total
|
|
|
|
@api.model
|
|
def create(self, values):
|
|
line = super(PurchaseOrderLine, self).create(values)
|
|
if line.order_id.state == 'purchase':
|
|
line._create_or_update_picking(values)
|
|
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):
|
|
if 'product_qty' in values:
|
|
self.filtered(lambda l: l.order_id.state == 'purchase')._create_or_update_picking(values)
|
|
result = super(PurchaseOrderLine, self).write(values)
|
|
# Update expected 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']})
|
|
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.Float(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 _create_or_update_picking(self, vals):
|
|
for line in self:
|
|
if line.product_id.type in ('product', 'consu'):
|
|
if float_compare(vals['product_qty'], line.qty_invoiced, line.product_uom.rounding) == -1:
|
|
# If the quantity is now below the invoiced quantity, create an activity on the vendor bill
|
|
# inviting the user to create a refund.
|
|
activity = self.env['mail.activity'].sudo().create({
|
|
'activity_type_id': self.env.ref('mail.mail_activity_data_todo').id,
|
|
'note': _('The quantities on your purchase order indicate less than billed. You should ask for a refund. '),
|
|
'res_id': line.invoice_lines[0].invoice_id.id,
|
|
'res_model_id': self.env.ref('account.model_account_invoice').id,
|
|
})
|
|
activity._onchange_activity_type_id()
|
|
diff_purchase_uom_qty = vals['product_qty'] - line.product_qty
|
|
diff_qty = line.product_uom._compute_quantity(diff_purchase_uom_qty, line.product_id.uom_id)
|
|
if diff_purchase_uom_qty > 0 or not line.move_ids:
|
|
# If the user increased quantity of existing line or created a new line
|
|
pickings = line.order_id.picking_ids.filtered(lambda x: x.state not in ('done', 'cancel') and x.location_dest_id.usage in ('internal', 'transit'))
|
|
picking = pickings and pickings[0] or False
|
|
if not picking:
|
|
res = line.order_id._prepare_picking()
|
|
picking = self.env['stock.picking'].create(res)
|
|
move_vals = line._get_move_template(picking)
|
|
if not line.move_ids:
|
|
move_vals['product_uom_qty'] = vals['product_qty']
|
|
else:
|
|
move_vals['product_uom_qty'] = diff_purchase_uom_qty
|
|
self.env['stock.move']\
|
|
.create(move_vals)\
|
|
.action_confirm()\
|
|
.action_assign()
|
|
else:
|
|
# Prevent decreasing below received quantity
|
|
if float_compare(vals['product_qty'], line.qty_received, line.product_uom.rounding) >= 0:
|
|
open_moves = line.move_ids.filtered(lambda m: m.state not in ('done', 'cancel')).sorted(lambda m: m.procurement_id.id)
|
|
# We sort by procurement id to decrease first the moves without procurement id,
|
|
# the idea is to get rid of the extra moves in priority
|
|
for move in open_moves:
|
|
if float_compare(move.product_qty, -diff_qty, line.product_id.uom_id.rounding) > 0:
|
|
qty_for_move = line.product_uom._compute_quantity(diff_purchase_uom_qty, move.product_uom)
|
|
move.product_uom_qty += qty_for_move
|
|
move.action_assign()
|
|
break
|
|
else:
|
|
diff_qty += move.product_qty
|
|
move.action_cancel()
|
|
else:
|
|
raise UserError('You cannot decrease the ordered quantity below the received quantity.\n'
|
|
'Create a return first.')
|
|
|
|
@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)['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 _get_move_template(self, picking):
|
|
self.ensure_one()
|
|
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_ids': False,
|
|
'state': 'draft',
|
|
'purchase_line_id': self.id,
|
|
'company_id': self.order_id.company_id.id,
|
|
'price_unit': self._get_stock_move_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,
|
|
}
|
|
return template
|
|
|
|
@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
|
|
for move in self.move_ids.filtered(lambda x: x.state != 'cancel' and not x.location_dest_id.usage == "supplier"):
|
|
qty += move.product_qty
|
|
template = self._get_move_template(picking)
|
|
# 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_ids': procurement.move_dest_id and [(4, procurement.move_dest_id.id)] or [], # 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 Model seller: used to fetch the delivery delay (if no seller
|
|
is provided, the delay is 0)
|
|
:param Model 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(seller.price, self.product_id.supplier_taxes_id, self.taxes_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
|
|
|
|
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(seller.price, procurement.purchase_line_id.product_id.supplier_taxes_id, procurement.purchase_line_id.taxes_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, partner, 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 = self.product_id._select_seller(
|
|
partner_id=partner,
|
|
quantity=self.product_qty,
|
|
date=fields.Date.to_string(schedule_date),
|
|
uom_id=self.product_uom)
|
|
|
|
return schedule_date - relativedelta(days=int(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(seller.price, self.product_id.supplier_taxes_id, taxes_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(partner, schedule_date)
|
|
fpos = self.env['account.fiscal.position'].with_context(company_id=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(seller.price, line.product_id.supplier_taxes_id, line.taxes_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 Policy",
|
|
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 send_mail(self, auto_commit=False):
|
|
if self._context.get('default_model') == 'purchase.order' and self._context.get('default_res_id'):
|
|
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'
|
|
return super(MailComposeMessage, self.with_context(mail_post_autofollow=True)).send_mail(auto_commit=auto_commit)
|