When we have purchase orders and we return some products to the supplier, we now have the possibilty to set those moves as 'to refund' and so the quantity received is decreased. Because the feature 'to refund' already exists on the sale orders, we will generalize it in the module 'stock_account' which is a dependency of both 'sale_stock' and 'purchase' which are the both module that use the feature 'to refund'.
1148 lines
55 KiB
Python
1148 lines
55 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', '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')
|
|
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 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', '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, 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, oldname='minimum_planned_date')
|
|
|
|
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('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['report'].get_action(self, 'purchase.report_purchasequotation')
|
|
|
|
@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()
|
|
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 operation 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 = '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.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.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.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)
|
|
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.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 _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 _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' and not x.location_dest_id.usage == "supplier"):
|
|
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:
|
|
# 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(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
|
|
|
|
@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(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 == '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]
|
|
|
|
@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
|
|
|
|
gpo = procurement.rule_id.group_propagation_option
|
|
group = (gpo == 'fixed' and procurement.rule_id.group_id) or \
|
|
(gpo == 'propagate' and procurement.group_id) or False
|
|
|
|
domain = (
|
|
('partner_id', '=', partner.id),
|
|
('state', '=', 'draft'),
|
|
('picking_type_id', '=', procurement.rule_id.picking_type_id.id),
|
|
('company_id', '=', procurement.company_id.id),
|
|
('dest_address_id', '=', procurement.partner_dest_id.id))
|
|
if group:
|
|
domain += (('group_id', '=', group.id),)
|
|
|
|
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 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 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)
|