The feature "set all date to all order line" on a purchase order allow
user to update the schedulded date of all order lines.
It was backported in 9.0 with 401a3cfa but in 9.0 date_planned was not
stored, the button would wrongly set the scheduled dates of the lines to
the minimum date.
With this commit the functionality should work as expected, unless
someone use an out-of-date template in which case the functionnality is
disabled.
opw-697680
note: PO date_planned is stored after 9.0 so this commit must not be
forward-ported.
1040 lines
50 KiB
Python
1040 lines
50 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 openerp import api, fields, models, _, SUPERUSER_ID
|
|
from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT
|
|
from openerp.tools.translate import _
|
|
from openerp.tools.float_utils import float_is_zero, float_compare
|
|
import openerp.addons.decimal_precision as dp
|
|
from openerp.exceptions import UserError, AccessError
|
|
|
|
class PurchaseOrder(models.Model):
|
|
_name = "purchase.order"
|
|
_inherit = ['mail.thread', 'ir.needaction_mixin']
|
|
_description = "Purchase Order"
|
|
_order = 'date_order desc, id desc'
|
|
|
|
@api.depends('order_line.price_total')
|
|
def _amount_all(self):
|
|
for order in self:
|
|
amount_untaxed = amount_tax = 0.0
|
|
for line in order.order_line:
|
|
amount_untaxed += line.price_subtotal
|
|
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.product_qty')
|
|
def _get_invoiced(self):
|
|
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
|
for order in self:
|
|
if order.state != 'purchase':
|
|
order.invoice_status = 'no'
|
|
continue
|
|
|
|
if any(float_compare(line.qty_invoiced, line.product_qty, 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, 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.state')
|
|
def _compute_invoice(self):
|
|
for order in self:
|
|
invoices = self.env['account.invoice']
|
|
for line in order.order_line:
|
|
invoices |= line.invoice_lines.mapped('invoice_id')
|
|
order.invoice_ids = invoices
|
|
order.invoice_count = len(invoices)
|
|
|
|
@api.model
|
|
def _default_picking_type(self):
|
|
type_obj = self.env['stock.picking.type']
|
|
company_id = self.env.context.get('company_id') or self.env.user.company_id.id
|
|
types = type_obj.search([('code', '=', 'incoming'), ('warehouse_id.company_id', '=', company_id)])
|
|
if not types:
|
|
types = type_obj.search([('code', '=', 'incoming'), ('warehouse_id', '=', False)])
|
|
return types[:1]
|
|
|
|
@api.depends('order_line.move_ids')
|
|
def _compute_picking(self):
|
|
for order in self:
|
|
pickings = self.env['stock.picking']
|
|
for line in order.order_line:
|
|
# We keep a limited scope on purpose. Ideally, we should also use move_orig_ids and
|
|
# do some recursive search, but that could be prohibitive if not done correctly.
|
|
moves = line.move_ids | line.move_ids.mapped('returned_move_ids')
|
|
moves = moves.filtered(lambda r: r.state != 'cancel')
|
|
pickings |= moves.mapped('picking_id')
|
|
order.picking_ids = pickings
|
|
order.picking_count = len(pickings)
|
|
|
|
READONLY_STATES = {
|
|
'purchase': [('readonly', True)],
|
|
'done': [('readonly', True)],
|
|
'cancel': [('readonly', True)],
|
|
}
|
|
|
|
name = fields.Char('Order Reference', required=True, index=True, copy=False, default='New')
|
|
origin = fields.Char('Source Document', copy=False,\
|
|
help="Reference of the document that generated this purchase order "
|
|
"request (e.g. a sale order or an internal procurement request)")
|
|
partner_ref = fields.Char('Vendor Reference', copy=False,\
|
|
help="Reference of the sales order or bid sent by the vendor. "
|
|
"It's used to do the matching when you receive the "
|
|
"products as this reference is usually written on the "
|
|
"delivery order sent by your vendor.")
|
|
date_order = fields.Datetime('Order Date', required=True, states=READONLY_STATES, index=True, copy=False, default=fields.Datetime.now,\
|
|
help="Depicts the date where the Quotation should be validated and converted into a purchase order.")
|
|
date_approve = fields.Date('Approval Date', readonly=1, index=True, copy=False)
|
|
partner_id = fields.Many2one('res.partner', string='Vendor', required=True, states=READONLY_STATES, change_default=True, track_visibility='always')
|
|
dest_address_id = fields.Many2one('res.partner', string='Drop Ship Address', states=READONLY_STATES,\
|
|
help="Put an address if you want to deliver directly from the vendor to the customer. "\
|
|
"Otherwise, keep empty to deliver to your own company.")
|
|
currency_id = fields.Many2one('res.currency', 'Currency', required=True, states=READONLY_STATES,\
|
|
default=lambda self: self.env.user.company_id.currency_id.id)
|
|
state = fields.Selection([
|
|
('draft', 'Draft PO'),
|
|
('sent', 'RFQ Sent'),
|
|
('to approve', 'To Approve'),
|
|
('purchase', 'Purchase Order'),
|
|
('done', 'Done'),
|
|
('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=READONLY_STATES, copy=True)
|
|
notes = fields.Text('Terms and Conditions')
|
|
|
|
invoice_count = fields.Integer(compute="_compute_invoice", string='# of Invoices', copy=False, default=0)
|
|
invoice_ids = fields.Many2many('account.invoice', compute="_compute_invoice", string='Invoices', copy=False)
|
|
invoice_status = fields.Selection([
|
|
('no', 'Not purchased'),
|
|
('to invoice', 'Waiting Invoices'),
|
|
('invoiced', 'Invoice Received'),
|
|
], string='Invoice Status', compute='_get_invoiced', store=True, readonly=True, copy=False, default='no')
|
|
|
|
picking_count = fields.Integer(compute='_compute_picking', string='Receptions', default=0)
|
|
picking_ids = fields.Many2many('stock.picking', compute='_compute_picking', string='Receptions', copy=False)
|
|
|
|
# There is no inverse function on purpose since the date may be different on each line
|
|
date_planned = fields.Datetime(string='Scheduled Date', compute='_compute_date_planned', required=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 Term')
|
|
incoterm_id = fields.Many2one('stock.incoterms', 'Incoterm', help="International Commercial Terms are a series of predefined commercial terms used in international transactions.")
|
|
|
|
product_id = fields.Many2one('product.product', related='order_line.product_id', string='Product')
|
|
create_uid = fields.Many2one('res.users', 'Responsible')
|
|
company_id = fields.Many2one('res.company', 'Company', required=True, index=True, states=READONLY_STATES, default=lambda self: self.env.user.company_id.id)
|
|
|
|
picking_type_id = fields.Many2one('stock.picking.type', 'Deliver To', states=READONLY_STATES, required=True, default=_default_picking_type,\
|
|
help="This will determine picking type of incoming shipment")
|
|
default_location_dest_id_usage = fields.Selection(related='picking_type_id.default_location_dest_id.usage', string='Destination Location Type',\
|
|
help="Technical field used to display the Drop Ship Address", readonly=True)
|
|
group_id = fields.Many2one('procurement.group', string="Procurement Group", copy=False)
|
|
|
|
@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+')'
|
|
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(
|
|
line.product_id, 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('picking_type_id')
|
|
def _onchange_picking_type_id(self):
|
|
if self.picking_type_id.default_location_dest_id.usage != 'customer':
|
|
self.dest_address_id = False
|
|
|
|
@api.multi
|
|
def action_rfq_send(self):
|
|
'''
|
|
This function opens a window to compose an email, with the edi purchase template message loaded by default
|
|
'''
|
|
self.ensure_one()
|
|
ir_model_data = self.env['ir.model.data']
|
|
try:
|
|
if self.env.context.get('send_rfq', False):
|
|
template_id = ir_model_data.get_object_reference('purchase', 'email_template_edi_purchase')[1]
|
|
else:
|
|
template_id = ir_model_data.get_object_reference('purchase', 'email_template_edi_purchase_done')[1]
|
|
except ValueError:
|
|
template_id = False
|
|
try:
|
|
compose_form_id = ir_model_data.get_object_reference('mail', 'email_compose_message_wizard_form')[1]
|
|
except ValueError:
|
|
compose_form_id = False
|
|
ctx = dict(self.env.context or {})
|
|
ctx.update({
|
|
'default_model': 'purchase.order',
|
|
'default_res_id': self.ids[0],
|
|
'default_use_template': bool(template_id),
|
|
'default_template_id': template_id,
|
|
'default_composition_mode': 'comment',
|
|
})
|
|
return {
|
|
'name': _('Compose Email'),
|
|
'type': 'ir.actions.act_window',
|
|
'view_type': 'form',
|
|
'view_mode': 'form',
|
|
'res_model': 'mail.compose.message',
|
|
'views': [(compose_form_id, 'form')],
|
|
'view_id': compose_form_id,
|
|
'target': 'new',
|
|
'context': ctx,
|
|
}
|
|
|
|
@api.multi
|
|
def print_quotation(self):
|
|
self.write({'state': "sent"})
|
|
return self.env['report'].get_action(self, 'purchase.report_purchasequotation')
|
|
|
|
@api.multi
|
|
def button_approve(self):
|
|
self.write({'state': 'purchase'})
|
|
self._create_picking()
|
|
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 {}
|
|
|
|
@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.i You must first cancel related vendor bills."))
|
|
|
|
for pick in order.picking_ids.filtered(lambda r: r.state != 'cancel'):
|
|
pick.action_cancel()
|
|
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_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):
|
|
for order in self:
|
|
if any([ptype in ['product', 'consu'] for ptype in order.order_line.mapped('product_id.type')]):
|
|
res = order._prepare_picking()
|
|
picking = self.env['stock.picking'].create(res)
|
|
moves = order.order_line.filtered(lambda r: r.product_id.type in ['product', 'consu'])._create_stock_moves(picking)
|
|
move_ids = moves.action_confirm()
|
|
moves = self.env['stock.move'].browse(move_ids)
|
|
moves.force_assign()
|
|
return True
|
|
|
|
@api.multi
|
|
def _add_supplier_to_product(self):
|
|
# Add the partner in the supplier list of the product if the supplier is not registered for
|
|
# this product. We limit to 10 the number of suppliers for a product to avoid the mess that
|
|
# could be caused for some generic products ("Miscellaneous").
|
|
for line in self.order_line:
|
|
# Do not add a contact as a supplier
|
|
partner = self.partner_id if not self.partner_id.parent_id else self.partner_id.parent_id
|
|
if partner not in line.product_id.seller_ids.mapped('name') and len(line.product_id.seller_ids) <= 10:
|
|
currency = partner.property_purchase_currency_id or self.env.user.company_id.currency_id
|
|
supplierinfo = {
|
|
'name': partner.id,
|
|
'sequence': max(line.product_id.seller_ids.mapped('sequence')) + 1 if line.product_id.seller_ids else 1,
|
|
'product_uom': line.product_uom.id,
|
|
'min_qty': 0.0,
|
|
'price': self.currency_id.compute(line.price_unit, currency),
|
|
'currency_id': currency.id,
|
|
'delay': 0,
|
|
}
|
|
vals = {
|
|
'seller_ids': [(0, 0, supplierinfo)],
|
|
}
|
|
try:
|
|
line.product_id.write(vals)
|
|
except AccessError: # no write access rights -> just ignore
|
|
break
|
|
|
|
@api.multi
|
|
def action_view_picking(self):
|
|
'''
|
|
This function returns an action that display existing picking orders of given purchase order ids.
|
|
When only one found, show the picking immediately.
|
|
'''
|
|
action = self.env.ref('stock.action_picking_tree')
|
|
result = action.read()[0]
|
|
|
|
#override the context to get rid of the default filtering on picking type
|
|
result['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):
|
|
# implementation for 9.0 where PO date_planned is not stored
|
|
date_planned = self.env.context.get('date_planned')
|
|
if not date_planned:
|
|
return
|
|
for order in self:
|
|
for line in order.order_line:
|
|
line.update({'date_planned': date_planned})
|
|
|
|
|
|
class PurchaseOrderLine(models.Model):
|
|
_name = 'purchase.order.line'
|
|
_description = 'Purchase Order Line'
|
|
|
|
@api.depends('product_qty', 'price_unit', 'taxes_id')
|
|
def _compute_amount(self):
|
|
for line in self:
|
|
taxes = line.taxes_id.compute_all(line.price_unit, line.order_id.currency_id, line.product_qty, product=line.product_id, partner=line.order_id.partner_id)
|
|
line.update({
|
|
'price_tax': taxes['total_included'] - taxes['total_excluded'],
|
|
'price_total': taxes['total_included'],
|
|
'price_subtotal': taxes['total_excluded'],
|
|
})
|
|
|
|
@api.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']:
|
|
qty += inv_line.uom_id._compute_qty_obj(inv_line.uom_id, inv_line.quantity, line.product_uom)
|
|
line.qty_invoiced = qty
|
|
|
|
@api.depends('order_id.state', 'move_ids.state')
|
|
def _compute_qty_received(self):
|
|
productuom = self.env['product.uom']
|
|
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
|
|
bom_delivered = self.sudo()._get_bom_delivered(line.sudo())
|
|
if bom_delivered and any(bom_delivered.values()):
|
|
total = line.product_qty
|
|
elif bom_delivered:
|
|
total = 0.0
|
|
else:
|
|
total = 0.0
|
|
for move in line.move_ids:
|
|
if move.state == 'done':
|
|
if move.product_uom != line.product_uom:
|
|
total += productuom._compute_qty_obj(move.product_uom, move.product_uom_qty, line.product_uom)
|
|
else:
|
|
total += move.product_uom_qty
|
|
line.qty_received = total
|
|
|
|
def _get_bom_delivered(self, line):
|
|
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
|
bom_delivered = {}
|
|
# There is no dependencies between purchase and mrp
|
|
if 'mrp.bom' in self.env:
|
|
# In the case of a kit, we need to check if all components are shipped. We use a all or
|
|
# nothing policy. A product can have several BoMs, we don't know which one was used when the
|
|
# delivery was created.
|
|
for bom in line.product_id.product_tmpl_id.bom_ids:
|
|
if bom.type != 'phantom':
|
|
continue
|
|
bom_delivered[bom.id] = False
|
|
product_uom_qty_bom = self.env['product.uom']._compute_qty_obj(line.product_uom, line.product_qty, bom.product_uom)
|
|
bom_exploded = self.env['mrp.bom']._bom_explode(bom, line.product_id, product_uom_qty_bom)[0]
|
|
for bom_line in bom_exploded:
|
|
qty = 0.0
|
|
for move in line.move_ids:
|
|
if move.state == 'done' and move.product_id.id == bom_line.get('product_id', False):
|
|
qty += self.env['product.uom']._compute_qty(move.product_uom.id, move.product_uom_qty, bom_line['product_uom'])
|
|
if float_compare(qty, bom_line['product_qty'], precision_digits=precision) < 0:
|
|
bom_delivered[bom.id] = False
|
|
break
|
|
else:
|
|
bom_delivered[bom.id] = True
|
|
return bom_delivered
|
|
|
|
|
|
|
|
name = fields.Text(string='Description', required=True)
|
|
product_qty = fields.Float(string='Quantity', digits=dp.get_precision('Product Unit of Measure'), required=True)
|
|
date_planned = fields.Datetime(string='Scheduled Date', required=True, index=True)
|
|
taxes_id = fields.Many2many('account.tax', string='Taxes', domain=['|', ('active', '=', False), ('active', '=', True)])
|
|
product_uom = fields.Many2one('product.uom', string='Product Unit of Measure', required=True)
|
|
product_id = fields.Many2one('product.product', string='Product', domain=[('purchase_ok', '=', True)], change_default=True, required=True)
|
|
move_ids = fields.One2many('stock.move', 'purchase_line_id', string='Reservation', readonly=True, ondelete='set null', copy=False)
|
|
price_unit = fields.Float(string='Unit Price', required=True, digits=dp.get_precision('Product Price'))
|
|
|
|
price_subtotal = fields.Monetary(compute='_compute_amount', string='Subtotal', store=True)
|
|
price_total = fields.Monetary(compute='_compute_amount', string='Total', store=True)
|
|
price_tax = fields.Monetary(compute='_compute_amount', string='Tax', store=True)
|
|
|
|
order_id = fields.Many2one('purchase.order', string='Order Reference', index=True, required=True, ondelete='cascade')
|
|
account_analytic_id = fields.Many2one('account.analytic.account', string='Analytic Account', domain=[('account_type', '=', 'normal')])
|
|
company_id = fields.Many2one('res.company', related='order_id.company_id', string='Company', store=True, readonly=True)
|
|
state = fields.Selection(related='order_id.state', stored=True)
|
|
|
|
invoice_lines = fields.One2many('account.invoice.line', 'purchase_line_id', string="Invoice Lines", readonly=True, copy=False)
|
|
|
|
# Replace by invoiced Qty
|
|
qty_invoiced = fields.Float(compute='_compute_qty_invoiced', string="Billed Qty", store=True)
|
|
qty_received = fields.Float(compute='_compute_qty_received', string="Received Qty", 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 _create_stock_moves(self, picking):
|
|
moves = self.env['stock.move']
|
|
done = self.env['stock.move'].browse()
|
|
for line in self:
|
|
price_unit = line._get_stock_move_price_unit()
|
|
|
|
template = {
|
|
'name': line.name or '',
|
|
'product_id': line.product_id.id,
|
|
'product_uom': line.product_uom.id,
|
|
'date': line.order_id.date_order,
|
|
'date_expected': line.date_planned,
|
|
'location_id': line.order_id.partner_id.property_stock_supplier.id,
|
|
'location_dest_id': line.order_id._get_destination_location(),
|
|
'picking_id': picking.id,
|
|
'partner_id': line.order_id.dest_address_id.id,
|
|
'move_dest_id': False,
|
|
'state': 'draft',
|
|
'purchase_line_id': line.id,
|
|
'company_id': line.order_id.company_id.id,
|
|
'price_unit': price_unit,
|
|
'picking_type_id': line.order_id.picking_type_id.id,
|
|
'group_id': line.order_id.group_id.id,
|
|
'procurement_id': False,
|
|
'origin': line.order_id.name,
|
|
'route_ids': line.order_id.picking_type_id.warehouse_id and [(6, 0, [x.id for x in line.order_id.picking_type_id.warehouse_id.route_ids])] or [],
|
|
'warehouse_id':line.order_id.picking_type_id.warehouse_id.id,
|
|
}
|
|
|
|
# Fullfill all related procurements with this po line
|
|
diff_quantity = line.product_qty
|
|
for procurement in line.procurement_ids:
|
|
procurement_qty = procurement.product_uom._compute_qty_obj(procurement.product_uom, procurement.product_qty, line.product_uom)
|
|
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,
|
|
})
|
|
done += moves.create(tmp)
|
|
diff_quantity -= min(procurement_qty, diff_quantity)
|
|
if float_compare(diff_quantity, 0.0, precision_rounding=line.product_uom.rounding) > 0:
|
|
template['product_uom_qty'] = diff_quantity
|
|
done += moves.create(template)
|
|
return done
|
|
|
|
@api.multi
|
|
def unlink(self):
|
|
for line in self:
|
|
if line.order_id.state in ['approved', '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_qty', 'product_uom')
|
|
def _onchange_quantity(self):
|
|
if not self.product_id:
|
|
return
|
|
|
|
seller = self.product_id._select_seller(
|
|
self.product_id,
|
|
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 = self.env['product.uom']._compute_price(seller.product_uom.id, price_unit, to_uom_id=self.product_uom.id)
|
|
|
|
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_qty_obj(other_proc.product_uom, 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(
|
|
procurement.product_id,
|
|
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 = self.env['product.uom']._compute_price(seller.product_uom.id, price_unit, to_uom_id=procurement.purchase_line_id.product_uom.id)
|
|
|
|
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.model
|
|
def _run(self, procurement):
|
|
if procurement.rule_id and procurement.rule_id.action == 'buy':
|
|
return procurement.make_po()
|
|
return super(ProcurementOrder, self)._run(procurement)
|
|
|
|
@api.model
|
|
def _check(self, procurement):
|
|
if procurement.purchase_line_id:
|
|
if not procurement.move_ids:
|
|
return False
|
|
return all(move.state == 'done' for move in procurement.move_ids)
|
|
return super(ProcurementOrder, self)._check(procurement)
|
|
|
|
@api.v8
|
|
def _get_purchase_schedule_date(self):
|
|
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
|
|
|
|
@api.v7
|
|
def _get_purchase_schedule_date(self, procurement):
|
|
"""Return the datetime value to use as Schedule Date (``date_planned``) for the
|
|
Purchase Order Lines created to satisfy the given procurement.
|
|
|
|
:param browse_record procurement: the procurement for which a PO will be created.
|
|
:rtype: datetime
|
|
:return: the desired Schedule Date for the PO lines
|
|
"""
|
|
procurement_date_planned = datetime.strptime(procurement.date_planned, DEFAULT_SERVER_DATETIME_FORMAT)
|
|
schedule_date = (procurement_date_planned - relativedelta(days=procurement.company_id.po_lead))
|
|
return schedule_date
|
|
|
|
@api.v8
|
|
def _get_purchase_order_date(self, schedule_date):
|
|
self.ensure_one()
|
|
seller_delay = int(self.product_id._select_seller(self.product_id).delay)
|
|
return schedule_date - relativedelta(days=seller_delay)
|
|
|
|
@api.v7
|
|
def _get_purchase_order_date(self, cr, uid, procurement, company, schedule_date, context=None):
|
|
"""Return the datetime value to use as Order Date (``date_order``) for the
|
|
Purchase Order created to satisfy the given procurement.
|
|
|
|
:param browse_record procurement: the procurement for which a PO will be created.
|
|
:param browse_report company: the company to which the new PO will belong to.
|
|
:param datetime schedule_date: desired Scheduled Date for the Purchase Order lines.
|
|
:rtype: datetime
|
|
:return: the desired Order Date for the PO
|
|
"""
|
|
seller_delay = int(procurement.product_id._select_seller(procurement.product_id).delay)
|
|
return schedule_date - relativedelta(days=seller_delay)
|
|
|
|
@api.multi
|
|
def _prepare_purchase_order_line(self, po, supplier):
|
|
self.ensure_one()
|
|
|
|
procurement_uom_po_qty = self.env['product.uom']._compute_qty_obj(self.product_uom, self.product_qty, self.product_id.uom_po_id)
|
|
seller = self.product_id._select_seller(
|
|
self.product_id,
|
|
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(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
|
|
}
|
|
|
|
@api.multi
|
|
def make_po(self):
|
|
cache = {}
|
|
res = []
|
|
for procurement in self:
|
|
suppliers = procurement.product_id.seller_ids.filtered(lambda r: 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 = suppliers[0]
|
|
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)
|
|
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})
|
|
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 = self.env['product.uom']._compute_qty_obj(procurement.product_uom, procurement.product_qty, procurement.product_id.uom_po_id)
|
|
seller = self.product_id._select_seller(
|
|
procurement.product_id,
|
|
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
|
|
|
|
|
|
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')
|
|
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_ok = fields.Boolean('Can be Purchased', default=True)
|
|
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", default="receive")
|
|
route_ids = fields.Many2many(default=lambda self: self._get_buy_route())
|
|
|
|
|
|
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')),
|
|
]
|
|
r = {}
|
|
for group in self.env['purchase.report'].read_group(domain, ['product_id', 'quantity'], ['product_id']):
|
|
r[group['product_id'][0]] = group['quantity']
|
|
for product in self:
|
|
product.purchase_count = r.get(product.id, 0)
|
|
return True
|
|
|
|
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.Model):
|
|
_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'):
|
|
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)
|