* All methods are converted into an api.multi style (using ids instead of a browse record or an id) * Removed api.v7 methods for two methods defined in purchase and overriden in stock_calendar, now used only in a true multi-style (old and new API) * Moved methods having orderpoint as primary object on the orderpoint model * Removed unused next_day field on resource.calendar model; it was introduced with hacks of resource.calendar, and never used. The whole override of resource models should be cleaned but will be done after migration of resource and stock.calendar.
1044 lines
50 KiB
Python
1044 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
|
|
from openerp.tools.misc import formatLang
|
|
from openerp.addons.base.res.res_partner import WARNING_MESSAGE, WARNING_HELP
|
|
|
|
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.multi
|
|
def _inverse_date_planned(self):
|
|
for order in self:
|
|
order.order_line.write({'date_planned': self.date_planned})
|
|
|
|
@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.picking_id')
|
|
def _compute_picking(self):
|
|
for order in self:
|
|
pickings = self.env['stock.picking']
|
|
for line in order.order_line:
|
|
moves = line.move_ids.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, select=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, select=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, select=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, select=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)
|
|
|
|
date_planned = fields.Datetime(string='Scheduled Date', compute='_compute_date_planned', inverse='_inverse_date_planned', store=True, select=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, select=1, 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")
|
|
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(
|
|
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('partner_id')
|
|
def onchange_partner_id_warning(self):
|
|
if not self.partner_id:
|
|
return
|
|
warning = {}
|
|
title = False
|
|
message = False
|
|
|
|
partner = self.partner_id
|
|
|
|
# If partner has no warning, check its company
|
|
if partner.purchase_warn == 'no-message' and partner.parent_id:
|
|
partner = partner.parent_id
|
|
|
|
if partner.purchase_warn != 'no-message':
|
|
# Block if partner only has warning but parent company is blocked
|
|
if partner.purchase_warn != 'block' and partner.parent_id and partner.parent_id.purchase_warn == 'block':
|
|
partner = partner.parent_id
|
|
title = _("Warning for %s") % partner.name
|
|
message = partner.purchase_warn_msg
|
|
warning = {
|
|
'title': title,
|
|
'message': message
|
|
}
|
|
if partner.purchase_warn == 'block':
|
|
self.update({'partner_id': False})
|
|
return {'warning': warning}
|
|
return {}
|
|
|
|
@api.onchange('picking_type_id')
|
|
def _onchange_picking_type_id(self):
|
|
if self.picking_type_id.default_location_dest_id.usage != 'customer':
|
|
self.dest_address_id = False
|
|
|
|
@api.multi
|
|
def action_rfq_send(self):
|
|
'''
|
|
This function opens a window to compose an email, with the edi purchase template message loaded by default
|
|
'''
|
|
self.ensure_one()
|
|
ir_model_data = self.env['ir.model.data']
|
|
try:
|
|
if self.env.context.get('send_rfq', False):
|
|
template_id = ir_model_data.get_object_reference('purchase', 'email_template_edi_purchase')[1]
|
|
else:
|
|
template_id = ir_model_data.get_object_reference('purchase', 'email_template_edi_purchase_done')[1]
|
|
except ValueError:
|
|
template_id = False
|
|
try:
|
|
compose_form_id = ir_model_data.get_object_reference('mail', 'email_compose_message_wizard_form')[1]
|
|
except ValueError:
|
|
compose_form_id = False
|
|
ctx = dict(self.env.context or {})
|
|
ctx.update({
|
|
'default_model': 'purchase.order',
|
|
'default_res_id': self.ids[0],
|
|
'default_use_template': bool(template_id),
|
|
'default_template_id': template_id,
|
|
'default_composition_mode': 'comment',
|
|
})
|
|
return {
|
|
'name': _('Compose Email'),
|
|
'type': 'ir.actions.act_window',
|
|
'view_type': 'form',
|
|
'view_mode': 'form',
|
|
'res_model': 'mail.compose.message',
|
|
'views': [(compose_form_id, 'form')],
|
|
'view_id': compose_form_id,
|
|
'target': 'new',
|
|
'context': ctx,
|
|
}
|
|
|
|
@api.multi
|
|
def print_quotation(self):
|
|
self.write({'state': "sent"})
|
|
return self.env['report'].get_action(self, 'purchase.report_purchasequotation')
|
|
|
|
@api.multi
|
|
def button_approve(self):
|
|
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:
|
|
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. 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_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
|
|
}
|
|
|
|
@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)
|
|
moves.action_confirm()
|
|
order.order_line.mapped('move_ids').force_assign()
|
|
picking.message_post_with_view('mail.message_origin_link',
|
|
values={'self': picking, 'origin': order},
|
|
subtype_id=self.env.ref('mail.mt_note').id)
|
|
return True
|
|
|
|
@api.multi
|
|
def _add_supplier_to_product(self):
|
|
# Add the partner in the supplier list of the product if the supplier is not registered for
|
|
# this product. We limit to 10 the number of suppliers for a product to avoid the mess that
|
|
# could be caused for some generic products ("Miscellaneous").
|
|
for line in self.order_line:
|
|
# Do not add a contact as a supplier
|
|
partner = self.partner_id if not self.partner_id.parent_id else self.partner_id.parent_id
|
|
if partner not in line.product_id.seller_ids.mapped('name') and len(line.product_id.seller_ids) <= 10:
|
|
currency = partner.property_purchase_currency_id or self.env.user.company_id.currency_id
|
|
supplierinfo = {
|
|
'name': partner.id,
|
|
'sequence': max(line.product_id.seller_ids.mapped('sequence')) + 1 if line.product_id.seller_ids else 1,
|
|
'product_uom': line.product_uom.id,
|
|
'min_qty': 0.0,
|
|
'price': self.currency_id.compute(line.price_unit, currency),
|
|
'currency_id': currency.id,
|
|
'delay': 0,
|
|
}
|
|
vals = {
|
|
'seller_ids': [(0, 0, supplierinfo)],
|
|
}
|
|
try:
|
|
line.product_id.write(vals)
|
|
except AccessError: # no write access rights -> just ignore
|
|
break
|
|
|
|
@api.multi
|
|
def action_view_picking(self):
|
|
'''
|
|
This function returns an action that display existing picking orders of given purchase order ids.
|
|
When only one found, show the picking immediately.
|
|
'''
|
|
action = self.env.ref('stock.action_picking_tree')
|
|
result = action.read()[0]
|
|
|
|
#override the context to get rid of the default filtering on picking type
|
|
result['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
|
|
|
|
|
|
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
|
|
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
|
|
|
|
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, select=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', select=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="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_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(
|
|
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.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, schedule_date):
|
|
"""Return the datetime value to use as Order Date (``date_order``) for the
|
|
Purchase Order created to satisfy the given procurement. """
|
|
self.ensure_one()
|
|
seller_delay = int(self.product_id._select_seller(self.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)
|
|
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 = 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",
|
|
help="On ordered quantities: Invoice this product based on ordered quantities.\n"
|
|
"On received quantities: Invoice this product 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')),
|
|
]
|
|
r = {}
|
|
for group in self.env['purchase.report'].read_group(domain, ['product_id', 'unit_quantity'], ['product_id']):
|
|
r[group['product_id'][0]] = group['unit_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.TransientModel):
|
|
_inherit = 'mail.compose.message'
|
|
|
|
@api.multi
|
|
def send_mail(self, auto_commit=False):
|
|
compose_internal = self.filtered('subtype_id.internal')
|
|
if self._context.get('default_model') == 'purchase.order' and self._context.get('default_res_id') and not compose_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)
|