[MOV] purchase_stock: move code from purchase

We want to break the dependency between stock and purchase
for our furtur developpement. For more modularity, a new
bridge module 'purchase_stock' is created.
This commti move part of business code, views, data, ...
related to stock management from purchase into purchase_stock
without changing any feature.

Task #47927
This commit is contained in:
jem-odoo
2018-05-23 10:13:49 +02:00
parent 8d4609a1db
commit e38ed7c780
42 changed files with 922 additions and 703 deletions
+1 -1
View File
@@ -9,7 +9,7 @@
'summary': 'Purchase Orders, Receipts, Vendor Bills',
'description': "",
'website': 'https://www.odoo.com/page/purchase',
'depends': ['stock_account'],
'depends': ['account'],
'data': [
'security/purchase_security.xml',
'security/ir.model.access.csv',
+1 -15
View File
@@ -18,17 +18,7 @@
<field name="res_model">purchase.order</field>
</record>
<!--
Procurement rules and routes
-->
<record id="route_warehouse0_buy" model='stock.location.route'>
<field name="name">Buy</field>
<field name="company_id"></field>
<field name="sequence">5</field>
</record>
<!-- Sequences for purchase.order -->
<!-- Sequences for purchase.order -->
<record id="seq_purchase_order" model="ir.sequence">
<field name="name">Purchase Order</field>
<field name="code">purchase.order</field>
@@ -37,9 +27,5 @@
<field name="company_id" eval="False"/>
</record>
<!-- enable purchase on main warehouse -->
<record id="stock.warehouse0" model="stock.warehouse">
<field name="buy_to_resupply" eval="True"/>
</record>
</data>
</odoo>
-24
View File
@@ -6,21 +6,6 @@
<field eval="[(4, ref('group_purchase_user'))]" name="groups_id"/>
</record>
<record id="stock.res_company_1" model="res.company">
<field eval="1.0" name="po_lead"/>
</record>
<record id="product.product_delivery_01" model="product.product">
<field name="route_ids" eval="[(4,ref('route_warehouse0_buy'))]"></field>
</record>
<record id="product.product_delivery_02" model="product.product">
<field name="route_ids" eval="[(4,ref('route_warehouse0_buy'))]"></field>
</record>
<record id="product.product_product_20" model="product.product">
<field name="route_ids" eval="[(4,ref('route_warehouse0_buy'))]"></field>
</record>
</data>
<record id="purchase_order_1" model="purchase.order">
@@ -184,14 +169,5 @@
]"/>
</record>
<record id="stock.stock_warehouse_shop0" model="stock.warehouse">
<field name="buy_to_resupply" eval="True"/>
</record>
<function model="stock.warehouse" name="write">
<value model="stock.warehouse" search="[('partner_id', '=', ref('stock.res_partner_company_1'))]"/>
<value eval="{'buy_to_resupply': True}"/>
</function>
</odoo>
+1 -115
View File
@@ -44,7 +44,7 @@ class AccountInvoice(models.Model):
('invoice_status', 'in', ['to invoice', 'no']),
('partner_id', 'child_of', self.partner_id.id),
('id', 'not in', purchase_ids.ids),
]}
]}
return result
def _prepare_invoice_line_from_po_line(self, line):
@@ -146,110 +146,6 @@ class AccountInvoice(models.Model):
self.currency_id = self.env.context['default_currency_id']
return res
@api.model
def invoice_line_move_line_get(self):
res = super(AccountInvoice, self).invoice_line_move_line_get()
if self.env.user.company_id.anglo_saxon_accounting:
if self.type in ['in_invoice', 'in_refund']:
for i_line in self.invoice_line_ids:
res.extend(self._anglo_saxon_purchase_move_lines(i_line, res))
return res
@api.model
def _anglo_saxon_purchase_move_lines(self, i_line, res):
"""Return the additional move lines for purchase invoices and refunds.
i_line: An account.invoice.line object.
res: The move line entries produced so far by the parent move_line_get.
"""
inv = i_line.invoice_id
company_currency = inv.company_id.currency_id
if i_line.product_id and i_line.product_id.valuation == 'real_time' and i_line.product_id.type == 'product':
# get the fiscal position
fpos = i_line.invoice_id.fiscal_position_id
# get the price difference account at the product
acc = i_line.product_id.property_account_creditor_price_difference
if not acc:
# if not found on the product get the price difference account at the category
acc = i_line.product_id.categ_id.property_account_creditor_price_difference_categ
acc = fpos.map_account(acc).id
# reference_account_id is the stock input account
reference_account_id = i_line.product_id.product_tmpl_id.get_product_accounts(fiscal_pos=fpos)['stock_input'].id
diff_res = []
# calculate and write down the possible price difference between invoice price and product price
for line in res:
if line.get('invl_id', 0) == i_line.id and reference_account_id == line['account_id']:
valuation_price_unit = i_line.product_id.uom_id._compute_price(i_line.product_id.standard_price, i_line.uom_id)
line_quantity = line['quantity']
if i_line.product_id.cost_method != 'standard' and i_line.purchase_line_id:
#for average/fifo/lifo costing method, fetch real cost price from incomming moves
valuation_price_unit = i_line.purchase_line_id.product_uom._compute_price(i_line.purchase_line_id.price_unit, i_line.uom_id)
stock_move_obj = self.env['stock.move']
valuation_stock_move = stock_move_obj.search([('purchase_line_id', '=', i_line.purchase_line_id.id), ('state', '=', 'done')])
if valuation_stock_move:
valuation_price_unit_total = 0
valuation_total_qty = 0
for val_stock_move in valuation_stock_move:
valuation_price_unit_total += abs(val_stock_move.price_unit) * val_stock_move.product_qty
valuation_total_qty += val_stock_move.product_qty
valuation_price_unit = valuation_price_unit_total / valuation_total_qty
valuation_price_unit = i_line.product_id.uom_id._compute_price(valuation_price_unit, i_line.uom_id)
line_quantity = valuation_total_qty
elif i_line.product_id.cost_method == 'real':
# In this condition, we have a real price-valuated product which has not yet been received
valuation_price_unit = i_line.purchase_line_id.price_unit
interim_account_price = valuation_price_unit * line_quantity
if inv.currency_id.id != company_currency.id:
# We express everyhting in the invoice currency
valuation_price_unit = company_currency._convert(valuation_price_unit, inv.currency_id, inv.company_id, inv.date_invoice or fields.Date.today(), round=False)
interim_account_price = company_currency._convert(interim_account_price, inv.currency_id, inv.company_id, inv.date_invoice or fields.Date.today(), round=False)
invoice_cur_prec = inv.currency_id.decimal_places
if float_compare(valuation_price_unit, i_line.price_unit, precision_digits=invoice_cur_prec) != 0 and float_compare(line['price_unit'], i_line.price_unit, precision_digits=invoice_cur_prec) == 0:
# price with discount and without tax included
price_unit = i_line.price_unit * (1 - (i_line.discount or 0.0) / 100.0)
tax_ids = []
if line['tax_ids']:
#line['tax_ids'] is like [(4, tax_id, None), (4, tax_id2, None)...]
taxes = self.env['account.tax'].browse([x[1] for x in line['tax_ids']])
price_unit = taxes.compute_all(price_unit, currency=inv.currency_id, quantity=1.0)['total_excluded']
for tax in taxes:
tax_ids.append((4, tax.id, None))
for child in tax.children_tax_ids:
if child.type_tax_use != 'none':
tax_ids.append((4, child.id, None))
price_before = line.get('price', 0.0)
price_unit_val_dif = price_unit - valuation_price_unit
price_val_dif = price_before - interim_account_price
if inv.currency_id.compare_amounts(i_line.price_unit, i_line.purchase_line_id.price_unit) != 0 and acc:
# If the unit prices have not changed and we have a
# valuation difference, it means this difference is due to exchange rates,
# so we don't create anything, the exchange rate entries will
# be processed automatically by the rest of the code.
diff_res.append({
'type': 'src',
'name': i_line.name[:64],
'price_unit': inv.currency_id.round(price_unit_val_dif),
'quantity': line_quantity,
'price': inv.currency_id.round(price_val_dif),
'account_id': acc,
'product_id': line['product_id'],
'uom_id': line['uom_id'],
'account_analytic_id': line['account_analytic_id'],
'tax_ids': tax_ids,
})
return diff_res
return []
@api.model
def create(self, vals):
invoice = super(AccountInvoice, self).create(vals)
@@ -273,16 +169,6 @@ class AccountInvoice(models.Model):
invoice.message_post(body=message)
return result
def _get_last_step_stock_moves(self):
""" Overridden from stock_account.
Returns the stock moves associated to this invoice."""
rslt = super(AccountInvoice, self)._get_last_step_stock_moves()
for invoice in self.filtered(lambda x: x.type == 'in_invoice'):
rslt += invoice.mapped('invoice_line_ids.purchase_line_id.move_ids').filtered(lambda x: x.state == 'done' and x.location_id.usage == 'supplier')
for invoice in self.filtered(lambda x: x.type == 'in_refund'):
rslt += invoice.mapped('invoice_line_ids.purchase_line_id.move_ids').filtered(lambda x: x.state == 'done' and x.location_dest_id.usage == 'supplier')
return rslt
class AccountInvoiceLine(models.Model):
""" Override AccountInvoice_line to add the link to the purchase order line it is related to"""
-8
View File
@@ -9,13 +9,6 @@ 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:
@@ -31,7 +24,6 @@ class ProductTemplate(models.Model):
('receive', 'On received quantities'),
], string="Control Policy", help="On ordered quantities: control bills based on ordered quantities.\n"
"On received quantities: control bills based on received quantity.", default="receive")
route_ids = fields.Many2many(default=lambda self: self._get_buy_route())
purchase_line_warn = fields.Selection(WARNING_MESSAGE, 'Purchase Order Line', help=WARNING_HELP, required=True, default="no-message")
purchase_line_warn_msg = fields.Text('Message for Purchase Order Line')
+33 -357
View File
@@ -6,10 +6,9 @@ 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.tools.float_utils import float_compare
from odoo.exceptions import UserError, AccessError
from odoo.tools.misc import formatLang
from odoo.addons.base.models.res_partner import WARNING_MESSAGE, WARNING_HELP
from odoo.addons import decimal_precision as dp
@@ -68,35 +67,6 @@ class PurchaseOrder(models.Model):
order.invoice_ids = invoices
order.invoice_count = len(invoices)
@api.model
def _default_picking_type(self):
type_obj = self.env['stock.picking.type']
company_id = self.env.context.get('company_id') or self.env.user.company_id.id
types = type_obj.search([('code', '=', 'incoming'), ('warehouse_id.company_id', '=', company_id)])
if not types:
types = type_obj.search([('code', '=', 'incoming'), ('warehouse_id', '=', False)])
return types[:1]
@api.depends('order_line.move_ids.returned_move_ids',
'order_line.move_ids.state',
'order_line.move_ids.picking_id')
def _compute_picking(self):
for order in self:
pickings = self.env['stock.picking']
for line in order.order_line:
# We keep a limited scope on purpose. Ideally, we should also use move_orig_ids and
# do some recursive search, but that could be prohibitive if not done correctly.
moves = line.move_ids | line.move_ids.mapped('returned_move_ids')
pickings |= moves.mapped('picking_id')
order.picking_ids = pickings
order.picking_count = len(pickings)
@api.depends('picking_ids', 'picking_ids.state')
def _compute_is_shipped(self):
for order in self:
if order.picking_ids and all([x.state == 'done' for x in order.picking_ids]):
order.is_shipped = True
READONLY_STATES = {
'purchase': [('readonly', True)],
'done': [('readonly', True)],
@@ -104,10 +74,10 @@ class PurchaseOrder(models.Model):
}
name = fields.Char('Order Reference', required=True, index=True, copy=False, default='New')
origin = fields.Char('Source Document', copy=False,\
origin = fields.Char('Source Document', copy=False,
help="Reference of the document that generated this purchase order "
"request (e.g. a sales order)")
partner_ref = fields.Char('Vendor Reference', copy=False,\
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 "
@@ -116,10 +86,10 @@ class PurchaseOrder(models.Model):
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. "\
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,\
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'),
@@ -128,7 +98,7 @@ class PurchaseOrder(models.Model):
('purchase', 'Purchase Order'),
('done', 'Locked'),
('cancel', 'Cancelled')
], string='Status', readonly=True, index=True, copy=False, default='draft', track_visibility='onchange')
], 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')
@@ -138,10 +108,7 @@ class PurchaseOrder(models.Model):
('no', 'Nothing to Bill'),
('to invoice', 'Waiting Bills'),
('invoiced', 'No Bill to Receive'),
], string='Billing Status', compute='_get_invoiced', store=True, readonly=True, copy=False, default='no')
picking_count = fields.Integer(compute='_compute_picking', string='Picking count', default=0, store=True)
picking_ids = fields.Many2many('stock.picking', compute='_compute_picking', string='Receptions', copy=False, store=True)
], string='Billing Status', compute='_get_invoiced', store=True, readonly=True, copy=False, default='no')
# 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)
@@ -152,19 +119,11 @@ class PurchaseOrder(models.Model):
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')
user_id = fields.Many2one('res.users', string='Purchase Representative', index=True, track_visibility='onchange', default=lambda self: self.env.user)
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")
website_url = fields.Char(
'Website URL', compute='_website_url',
help='The full URL to access the document through the website.')
@@ -189,7 +148,7 @@ class PurchaseOrder(models.Model):
for po in self:
name = po.name
if po.partner_ref:
name += ' ('+po.partner_ref+')'
name += ' (' + po.partner_ref + ')'
if self.env.context.get('show_total_amount') and po.amount_total:
name += ': ' + formatLang(self.env, po.amount_total, currency_obj=po.currency_id)
result.append((po.id, name))
@@ -201,21 +160,6 @@ class PurchaseOrder(models.Model):
vals['name'] = self.env['ir.sequence'].next_by_code('purchase.order') or '/'
return super(PurchaseOrder, self).create(vals)
def write(self, vals):
if vals.get('order_line') and self.state == 'purchase':
for order in self:
pre_order_line_qty = {order_line: order_line.product_qty for order_line in order.mapped('order_line')}
res = super(PurchaseOrder, self).write(vals)
if vals.get('order_line') and self.state == 'purchase':
for order in self:
to_log = {}
for order_line in order.order_line:
if pre_order_line_qty.get(order_line, False) and float_compare(pre_order_line_qty[order_line], order_line.product_qty, precision_rounding=order_line.product_uom.rounding) > 0:
to_log[order_line] = (order_line.product_qty, pre_order_line_qty[order_line])
if to_log:
order._log_decrease_ordered_quantity(to_log)
return res
@api.multi
def unlink(self):
for order in self:
@@ -224,7 +168,6 @@ class PurchaseOrder(models.Model):
return super(PurchaseOrder, self).unlink()
@api.multi
@api.returns('self', lambda value: value.id)
def copy(self, default=None):
new_po = super(PurchaseOrder, self).copy(default=default)
for line in new_po.order_line:
@@ -254,8 +197,7 @@ class PurchaseOrder(models.Model):
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
if not self.currency_id:
self.currency_id = self.partner_id.property_purchase_currency_id.id or self.env.user.company_id.currency_id.id
self.currency_id = self.partner_id.property_purchase_currency_id.id or self.env.user.company_id.currency_id.id
return {}
@api.onchange('fiscal_position_id')
@@ -289,17 +231,12 @@ class PurchaseOrder(models.Model):
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):
'''
@@ -325,7 +262,7 @@ class PurchaseOrder(models.Model):
'default_use_template': bool(template_id),
'default_template_id': template_id,
'default_composition_mode': 'comment',
'custom_layout': "mail.mail_notification_borders",
'custom_layout': "purchase.mail_template_data_notification_email_purchase_order",
'force_email': True,
'mark_rfq_as_sent': True,
})
@@ -355,9 +292,7 @@ class PurchaseOrder(models.Model):
@api.multi
def button_approve(self, force=False):
self.write({'state': 'purchase', 'date_approve': fields.Date.context_today(self)})
self._create_picking()
self.filtered(
lambda p: p.company_id.po_lock == 'lock').write({'state': 'done'})
self.filtered(lambda p: p.company_id.po_lock == 'lock').write({'state': 'done'})
return {}
@api.multi
@@ -385,26 +320,9 @@ class PurchaseOrder(models.Model):
@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."))
# If the product is MTO, change the procure_method of the the closest move to purchase to MTS.
# The purpose is to link the po that the user will manually generate to the existing moves's chain.
if order.state in ('draft', 'sent', 'to approve'):
for order_line in order.order_line:
if order_line.move_dest_ids:
siblings_states = (order_line.move_dest_ids.mapped('move_orig_ids')).mapped('state')
if all(state in ('done', 'cancel') for state in siblings_states):
order_line.move_dest_ids.write({'procure_method': 'make_to_stock'})
order_line.move_dest_ids._recompute_state()
for pick in order.picking_ids.filtered(lambda r: r.state != 'cancel'):
pick.action_cancel()
self.write({'state': 'cancel'})
@api.multi
@@ -415,55 +333,6 @@ class PurchaseOrder(models.Model):
def button_done(self):
self.write({'state': 'done'})
@api.multi
def _get_destination_location(self):
self.ensure_one()
if self.dest_address_id:
return self.dest_address_id.property_stock_customer.id
return self.picking_type_id.default_location_dest_id.id
@api.model
def _prepare_picking(self):
if not self.group_id:
self.group_id = self.group_id.create({
'name': self.name,
'partner_id': self.partner_id.id
})
if not self.partner_id.property_stock_supplier.id:
raise UserError(_("You must set a Vendor Location for this partner %s") % self.partner_id.name)
return {
'picking_type_id': self.picking_type_id.id,
'partner_id': self.partner_id.id,
'date': self.date_order,
'origin': self.name,
'location_dest_id': self._get_destination_location(),
'location_id': self.partner_id.property_stock_supplier.id,
'company_id': self.company_id.id,
}
@api.multi
def _create_picking(self):
StockPicking = self.env['stock.picking']
for order in self:
if any([ptype in ['product', 'consu'] for ptype in order.order_line.mapped('product_id.type')]):
pickings = order.picking_ids.filtered(lambda x: x.state not in ('done','cancel'))
if not pickings:
res = order._prepare_picking()
picking = StockPicking.create(res)
else:
picking = pickings[0]
moves = order.order_line._create_stock_moves(picking)
moves = moves.filtered(lambda x: x.state not in ('done', 'cancel'))._action_confirm()
seq = 0
for move in sorted(moves, key=lambda move: move.date_expected):
seq += 5
move.sequence = seq
moves._action_assign()
picking.message_post_with_view('mail.message_origin_link',
values={'self': picking, 'origin': order},
subtype_id=self.env.ref('mail.mt_note').id)
return True
@api.multi
def _add_supplier_to_product(self):
# Add the partner in the supplier list of the product if the supplier is not registered for
@@ -491,62 +360,6 @@ class PurchaseOrder(models.Model):
except AccessError: # no write access rights -> just ignore
break
def _log_decrease_ordered_quantity(self, purchase_order_lines_quantities):
def _keys_in_sorted(move):
""" sort by picking and the responsible for the product the
move.
"""
return (move.picking_id.id, move.product_id.responsible_id.id)
def _keys_in_groupby(move):
""" group by picking and the responsible for the product the
move.
"""
return (move.picking_id, move.product_id.responsible_id)
def _render_note_exception_quantity_po(order_exceptions):
order_line_ids = self.env['purchase.order.line'].browse([order_line.id for order in order_exceptions.values() for order_line in order[0]])
purchase_order_ids = order_line_ids.mapped('order_id')
move_ids = self.env['stock.move'].concat(*rendering_context.keys())
impacted_pickings = move_ids.mapped('picking_id')._get_impacted_pickings(move_ids) - move_ids.mapped('picking_id')
values = {
'purchase_order_ids': purchase_order_ids,
'order_exceptions': order_exceptions.values(),
'impacted_pickings': impacted_pickings,
}
return self.env.ref('purchase.exception_on_po').render(values=values)
documents = self.env['stock.picking']._log_activity_get_documents(purchase_order_lines_quantities, 'move_ids', 'DOWN', _keys_in_sorted, _keys_in_groupby)
filtered_documents = {}
for (parent, responsible), rendering_context in documents.items():
if parent._name == 'stock.picking':
if parent.state == 'cancel':
continue
filtered_documents[(parent, responsible)] = rendering_context
self.env['stock.picking']._log_activity(_render_note_exception_quantity_po, filtered_documents)
@api.multi
def action_view_picking(self):
'''
This function returns an action that display existing picking orders of given purchase order ids.
When only one found, show the picking immediately.
'''
action = self.env.ref('stock.action_picking_tree')
result = action.read()[0]
#override the context to get rid of the default filtering on operation type
result['context'] = {}
pick_ids = self.mapped('picking_ids')
#choose the view_mode accordingly
if len(pick_ids) > 1:
result['domain'] = "[('id','in',%s)]" % (pick_ids.ids)
elif len(pick_ids) == 1:
res = self.env.ref('stock.view_picking_form', False)
result['views'] = [(res and res.id or False, 'form')]
result['res_id'] = pick_ids.id
return result
@api.multi
def action_view_invoice(self):
'''
@@ -556,15 +369,15 @@ class PurchaseOrder(models.Model):
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',
# override the context to get rid of the default filtering
result['context'] = {
'type': 'in_invoice',
'default_purchase_id': self.id,
'default_currency_id': self.currency_id.id,
'default_company_id': self.company_id.id,
'company_id': self.company_id.id
}
#choose the view_mode accordingly
# 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:
@@ -614,33 +427,6 @@ class PurchaseOrderLine(models.Model):
qty -= inv_line.uom_id._compute_quantity(inv_line.quantity, line.product_uom)
line.qty_invoiced = qty
@api.model
def create(self, values):
line = super(PurchaseOrderLine, self).create(values)
if line.order_id.state == 'purchase':
line._create_or_update_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):
if 'product_qty' in values:
for line in self:
if line.order_id.state == 'purchase':
line.order_id.message_post_with_view('purchase.track_po_line_template',
values={'line': line, 'product_qty': values['product_qty']},
subtype_id=self.env.ref('mail.mt_note').id)
result = super(PurchaseOrderLine, self).write(values)
# Update expected date of corresponding moves
if 'date_planned' in values:
self.env['stock.move'].search([
('purchase_line_id', 'in', self.ids), ('state', '!=', 'done')
]).write({'date_expected': values['date_planned']})
if 'product_qty' in values:
self.filtered(lambda l: l.order_id.state == 'purchase')._create_or_update_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)
@@ -652,7 +438,6 @@ class PurchaseOrderLine(models.Model):
'Product Image', related="product_id.image",
help="Non-stored related field to allow portal user to see the image of the product he has ordered")
product_type = fields.Selection(related='product_id.type', readonly=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)
@@ -675,132 +460,29 @@ class PurchaseOrderLine(models.Model):
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)
orderpoint_id = fields.Many2one('stock.warehouse.orderpoint', 'Orderpoint')
move_dest_ids = fields.One2many('stock.move', 'created_purchase_line_id', 'Downstream Moves')
def _update_received_qty(self):
for line in self:
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':
msg = _("Extra line with %s ") % (line.product_id.display_name,)
line.order_id.message_post(body=msg)
return line
@api.multi
def _create_or_update_picking(self):
for line in self:
if line.product_id.type in ('product', 'consu'):
# Prevent decreasing below received quantity
if float_compare(line.product_qty, line.qty_received, line.product_uom.rounding) < 0:
raise UserError(_('You cannot decrease the ordered quantity below the received quantity.\n'
'Create a return first.'))
if float_compare(line.product_qty, line.qty_invoiced, line.product_uom.rounding) == -1:
# If the quantity is now below the invoiced quantity, create an activity on the vendor bill
# inviting the user to create a refund.
activity = self.env['mail.activity'].sudo().create({
'activity_type_id': self.env.ref('mail.mail_activity_data_todo').id,
'note': _('The quantities on your purchase order indicate less than billed. You should ask for a refund. '),
'res_id': line.invoice_lines[0].invoice_id.id,
'res_model_id': self.env.ref('account.model_account_invoice').id,
})
activity._onchange_activity_type_id()
# If the user increased quantity of existing line or created a new line
pickings = line.order_id.picking_ids.filtered(lambda x: x.state not in ('done', 'cancel') and x.location_dest_id.usage in ('internal', 'transit'))
picking = pickings and pickings[0] or False
if not picking:
res = line.order_id._prepare_picking()
picking = self.env['stock.picking'].create(res)
move_vals = line._prepare_stock_moves(picking)
for move_val in move_vals:
self.env['stock.move']\
.create(move_val)\
._action_confirm()\
._action_assign()
@api.multi
def _get_stock_move_price_unit(self):
self.ensure_one()
line = self[0]
order = line.order_id
price_unit = line.price_unit
if line.taxes_id:
price_unit = line.taxes_id.with_context(round=False).compute_all(
price_unit, currency=line.order_id.currency_id, quantity=1.0, product=line.product_id, partner=line.order_id.partner_id
)['total_excluded']
if line.product_uom.id != line.product_id.uom_id.id:
price_unit *= line.product_uom.factor / line.product_id.uom_id.factor
if order.currency_id != order.company_id.currency_id:
price_unit = order.currency_id._convert(
price_unit, order.company_id.currency_id, self.company_id, self.date_order or fields.Date.today(), 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_uom._compute_quantity(move.product_uom_qty, self.product_uom, rounding_method='HALF-UP')
template = {
'name': self.name or '',
'product_id': self.product_id.id,
'product_uom': self.product_uom.id,
'date': self.order_id.date_order,
'date_expected': self.date_planned,
'location_id': self.order_id.partner_id.property_stock_supplier.id,
'location_dest_id': self.order_id._get_destination_location(),
'picking_id': picking.id,
'partner_id': self.order_id.dest_address_id.id,
'move_dest_ids': [(4, x) for x in self.move_dest_ids.ids],
'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,
'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,
}
diff_quantity = self.product_qty - qty
if float_compare(diff_quantity, 0.0, precision_rounding=self.product_uom.rounding) > 0:
quant_uom = self.product_id.uom_id
get_param = self.env['ir.config_parameter'].sudo().get_param
if self.product_uom.id != quant_uom.id and get_param('stock.propagate_uom') != '1':
product_qty = self.product_uom._compute_quantity(diff_quantity, quant_uom, rounding_method='HALF-UP')
template['product_uom'] = quant_uom.id
template['product_uom_qty'] = product_qty
else:
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
def write(self, values):
if 'product_qty' in values:
for line in self:
if line.order_id.state == 'purchase':
line.order_id.message_post_with_view('purchase.track_po_line_template',
values={'line': line, 'product_qty': values['product_qty']},
subtype_id=self.env.ref('mail.mt_note').id)
return super(PurchaseOrderLine, self).write(values)
@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,))
raise UserError(_('Cannot delete a purchase order line which is in state \'%s\'.') % (line.state,))
return super(PurchaseOrderLine, self).unlink()
@api.model
@@ -822,12 +504,6 @@ class PurchaseOrderLine(models.Model):
else:
return datetime.today() + relativedelta(days=seller.delay if seller else 0)
def _merge_in_existing_line(self, product_id, product_qty, product_uom, location_id, name, origin, values):
""" This function purpose is to be override with the purpose to forbide _run_buy method
to merge a new po line in an existing one.
"""
return True
@api.onchange('product_id')
def onchange_product_id(self):
result = {}
@@ -20,13 +20,10 @@ class ResConfigSettings(models.TransientModel):
], string="Bill Control", default_model="product.template",
help="This default value is applied to any new product created. "
"This can be changed in the product detail form.", default="receive")
module_purchase_requisition = fields.Boolean("Purchase Agreements")
group_warning_purchase = fields.Boolean("Purchase Warnings", implied_group='purchase.group_warning_purchase')
module_stock_dropshipping = fields.Boolean("Dropshipping")
group_manage_vendor_price = fields.Boolean("Vendor Pricelists",
implied_group="purchase.group_manage_vendor_price")
module_account_3way_match = fields.Boolean("3-way matching: purchases, receptions and bills")
is_installed_sale = fields.Boolean(string="Is the Sale Module Installed")
po_lead = fields.Float(related='company_id.po_lead')
use_po_lead = fields.Boolean(
string="Security Lead Time for Purchase",
@@ -39,14 +36,6 @@ class ResConfigSettings(models.TransientModel):
if not self.use_po_lead:
self.po_lead = 0.0
@api.multi
def get_values(self):
res = super(ResConfigSettings, self).get_values()
res.update(
is_installed_sale=self.env['ir.module.module'].search([('name', '=', 'sale'), ('state', '=', 'installed')]).id,
)
return res
def set_values(self):
super(ResConfigSettings, self).set_values()
self.po_lock = 'lock' if self.lock_confirmed_po else 'edit'
@@ -6,18 +6,12 @@
<div class="page">
<div class="oe_structure"/>
<div class="row">
<div class="col-xs-6">
<div class="col-xs-6" name="purchase_shipping_address">
<strong>Shipping address:</strong>
<div t-if="o.dest_address_id">
<div t-field="o.dest_address_id"
t-options='{"widget": "contact", "fields": ["address", "name", "phone"], "no_marker": True, "phone_icons": True}'/>
</div>
<div t-if="not o.dest_address_id and o.picking_type_id and o.picking_type_id.warehouse_id">
<span t-field="o.picking_type_id.warehouse_id.name"/>
<div t-field="o.picking_type_id.warehouse_id.partner_id"
t-options='{"widget": "contact", "fields": ["address", "phone"], "no_marker": True, "phone_icons": True}'/>
</div>
</div>
<div class="col-xs-5 col-xs-offset-1">
<div t-field="o.partner_id"
@@ -25,6 +19,7 @@
<p t-if="o.partner_id.vat"><t t-esc="o.company_id.country_id.vat_label or 'TIN'"/>: <span t-field="o.partner_id.vat"/></p>
</div>
</div>
<h2 t-if="o.state == 'draft'">Request for Quotation #<span t-field="o.name"/></h2>
<h2 t-if="o.state in ['sent', 'to approve']">Purchase Order #<span t-field="o.name"/></h2>
<h2 t-if="o.state in ['purchase', 'done']">Purchase Order Confirmation #<span t-field="o.name"/></h2>
@@ -7,17 +7,12 @@
<div class="oe_structure"/>
<div class="row mt32 mb32">
<div class="col-xs-6">
<div class="col-xs-6" name="purchase_shipping_address">
<strong>Shipping address:</strong>
<div t-if="o.dest_address_id">
<div t-field="o.dest_address_id"
t-options='{"widget": "contact", "fields": ["address", "name", "phone"], "no_marker": True, "phone_icons": True}'/>
</div>
<div t-if="not o.dest_address_id and o.picking_type_id.warehouse_id">
<span t-field="o.picking_type_id.warehouse_id.name"/>
<div t-field="o.picking_type_id.warehouse_id.partner_id"
t-options='{"widget": "contact", "fields": ["address", "phone"], "no_marker": True, "phone_icons": True}'/>
</div>
</div>
<div class="col-xs-5 col-xs-offset-1">
<div t-field="o.partner_id"
@@ -67,4 +62,3 @@
</t>
</template>
</odoo>
+2 -6
View File
@@ -24,7 +24,6 @@ class PurchaseReport(models.Model):
('cancel', 'Cancelled')
], 'Order Status', readonly=True)
product_id = fields.Many2one('product.product', 'Product', readonly=True)
picking_type_id = fields.Many2one('stock.warehouse', 'Warehouse', readonly=True)
partner_id = fields.Many2one('res.partner', 'Vendor', readonly=True)
date_approve = fields.Date('Date Approved', readonly=True)
product_uom = fields.Many2one('uom.uom', 'Reference Unit of Measure', required=True)
@@ -88,8 +87,7 @@ class PurchaseReport(models.Model):
partner.commercial_partner_id as commercial_partner_id,
analytic_account.id as account_analytic_id,
sum(p.weight * l.product_qty/u.factor*u2.factor) as weight,
sum(p.volume * l.product_qty/u.factor*u2.factor) as volume,
spt.warehouse_id as picking_type_id
sum(p.volume * l.product_qty/u.factor*u2.factor) as volume
""" % self.env['res.currency']._select_companies_rates()
return select_str
@@ -108,7 +106,6 @@ class PurchaseReport(models.Model):
cr.company_id = s.company_id and
cr.date_start <= coalesce(s.date_order, now()) and
(cr.date_end is null or cr.date_end > coalesce(s.date_order, now())))
left join stock_picking_type spt on (spt.id=s.picking_type_id)
"""
return from_str
@@ -138,7 +135,6 @@ class PurchaseReport(models.Model):
u2.factor,
partner.country_id,
partner.commercial_partner_id,
analytic_account.id,
spt.warehouse_id
analytic_account.id
"""
return group_by_str
@@ -60,7 +60,6 @@
<field name="product_id"/>
<group expand="0" string="Extended Filters">
<field name="user_id"/>
<field name="picking_type_id"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="date_order"/>
<field name="date_approve"/>
@@ -91,7 +90,7 @@
<field name="help">Purchase Analysis allows you to easily check and analyse your company purchase history and performance. From this menu you can track your negotiation performance, the delivery performance of your vendors, etc.</field>
</record>
<menuitem id="purchase_report" name="Reporting" parent="menu_purchase_root" sequence="99"
<menuitem id="purchase_report" name="Reporting" parent="purchase.menu_purchase_root" sequence="99"
groups="purchase.group_purchase_manager" action="action_purchase_order_report_all"/>
</odoo>
@@ -1,25 +1,13 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_purchase_order,purchase.order,model_purchase_order,group_purchase_user,1,1,1,1
access_purchase_order_manager,purchase.order,model_purchase_order,group_purchase_manager,1,1,1,1
access_purchase_order_stock_worker,purchase.order,model_purchase_order,stock.group_stock_user,1,0,0,0
access_purchase_order_invoicing_payments,purchase.order,model_purchase_order,account.group_account_invoice,1,1,0,0
access_purchase_order_portal,purchase.order.portal,purchase.model_purchase_order,base.group_portal,1,0,0,0
access_purchase_order_line,purchase.order.line user,model_purchase_order_line,group_purchase_user,1,1,1,1
access_purchase_order_line_manager,purchase.order.line,model_purchase_order_line,group_purchase_manager,1,1,1,1
access_purchase_order_line_stock_worker,purchase.order.line,model_purchase_order_line,stock.group_stock_user,1,0,0,0
access_purchase_order_line_invoicing_payments,purchase.order.line,model_purchase_order_line,account.group_account_invoice,1,1,0,0
access_purchase_order_line_portal,purchase.order.line.portal,purchase.model_purchase_order_line,base.group_portal,1,0,0,0
access_stock_location_purchase_user,stock.location,stock.model_stock_location,group_purchase_user,1,0,0,0
access_stock_warehouse_purchase_user,stock.warehouse,stock.model_stock_warehouse,group_purchase_user,1,0,0,0
access_stock_picking_purchase_user,stock.picking,stock.model_stock_picking,group_purchase_user,1,1,1,1
access_stock_move_purchase_user,stock.move,stock.model_stock_move,group_purchase_user,1,1,1,0
access_account_tax_purchase_user,account.tax,account.model_account_tax,group_purchase_user,1,0,0,0
access_report_purchase_order,purchase.report,model_purchase_report,group_purchase_manager,1,0,0,0
access_report_purchase_order_user,purchase.report user,model_purchase_report,group_purchase_user,1,0,0,0
access_stock_location_purchase_user_manager,stock.location,stock.model_stock_location,group_purchase_manager,1,0,0,0
access_stock_warehouse_purchase_user_manager,stock.warehouse,stock.model_stock_warehouse,group_purchase_manager,1,0,0,0
access_stock_picking_purchase_user_manager,stock.picking,stock.model_stock_picking,group_purchase_manager,1,1,1,1
access_stock_move_purchase_user_manager,stock.move,stock.model_stock_move,group_purchase_manager,1,1,1,1
access_account_tax_purchase_user_manager,account.tax,account.model_account_tax,group_purchase_manager,1,0,0,0
access_product_product_purchase_user,product.product.purchase.user,product.model_product_product,group_purchase_user,1,0,0,0
access_product_product_purchase_manager,product.product purchase_manager,product.model_product_product,purchase.group_purchase_manager,1,1,1,1
@@ -47,9 +35,6 @@ access_product_group_res_partner_purchase_manager,res_partner group_purchase_man
access_product_pricelist_item_purchase_manager,product.pricelist.item purchase_manager,product.model_product_pricelist_item,purchase.group_purchase_manager,1,1,1,1
access_account_account_purchase_manager,account.account purchase manager,account.model_account_account,purchase.group_purchase_manager,1,0,0,0
access_account_journal_purchase_manager,account.journal purchase manager,account.model_account_journal,purchase.group_purchase_manager,1,0,0,0
access_stock_location_purchase_manager,stock.location purchase manager,stock.model_stock_location,purchase.group_purchase_manager,1,0,0,0
access_stock_warehouse_orderpoint_manager,stock.warehouse.orderpoint,stock.model_stock_warehouse_orderpoint,purchase.group_purchase_manager,1,0,0,0
access_stock_warehouse_orderpoint_user,stock.warehouse.orderpoint,stock.model_stock_warehouse_orderpoint,group_purchase_user,1,0,0,0
access_product_price_history_purchase_user,prices.history purchase user,product.model_product_price_history,purchase.group_purchase_user,1,0,0,0
access_product_price_history_purchase_manager,prices.history purchase manager,product.model_product_price_history,purchase.group_purchase_manager,1,1,1,1
access_purchase_bill_union,access_purchase_bill_union,model_purchase_bill_union,purchase.group_purchase_user,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_purchase_order purchase.order model_purchase_order group_purchase_user 1 1 1 1
3 access_purchase_order_manager purchase.order model_purchase_order group_purchase_manager 1 1 1 1
access_purchase_order_stock_worker purchase.order model_purchase_order stock.group_stock_user 1 0 0 0
4 access_purchase_order_invoicing_payments purchase.order model_purchase_order account.group_account_invoice 1 1 0 0
5 access_purchase_order_portal purchase.order.portal purchase.model_purchase_order base.group_portal 1 0 0 0
6 access_purchase_order_line purchase.order.line user model_purchase_order_line group_purchase_user 1 1 1 1
7 access_purchase_order_line_manager purchase.order.line model_purchase_order_line group_purchase_manager 1 1 1 1
access_purchase_order_line_stock_worker purchase.order.line model_purchase_order_line stock.group_stock_user 1 0 0 0
8 access_purchase_order_line_invoicing_payments purchase.order.line model_purchase_order_line account.group_account_invoice 1 1 0 0
9 access_purchase_order_line_portal purchase.order.line.portal purchase.model_purchase_order_line base.group_portal 1 0 0 0
access_stock_location_purchase_user stock.location stock.model_stock_location group_purchase_user 1 0 0 0
access_stock_warehouse_purchase_user stock.warehouse stock.model_stock_warehouse group_purchase_user 1 0 0 0
access_stock_picking_purchase_user stock.picking stock.model_stock_picking group_purchase_user 1 1 1 1
access_stock_move_purchase_user stock.move stock.model_stock_move group_purchase_user 1 1 1 0
10 access_account_tax_purchase_user account.tax account.model_account_tax group_purchase_user 1 0 0 0
access_report_purchase_order purchase.report model_purchase_report group_purchase_manager 1 0 0 0
access_report_purchase_order_user purchase.report user model_purchase_report group_purchase_user 1 0 0 0
access_stock_location_purchase_user_manager stock.location stock.model_stock_location group_purchase_manager 1 0 0 0
access_stock_warehouse_purchase_user_manager stock.warehouse stock.model_stock_warehouse group_purchase_manager 1 0 0 0
access_stock_picking_purchase_user_manager stock.picking stock.model_stock_picking group_purchase_manager 1 1 1 1
access_stock_move_purchase_user_manager stock.move stock.model_stock_move group_purchase_manager 1 1 1 1
11 access_account_tax_purchase_user_manager account.tax account.model_account_tax group_purchase_manager 1 0 0 0
12 access_product_product_purchase_user product.product.purchase.user product.model_product_product group_purchase_user 1 0 0 0
13 access_product_product_purchase_manager product.product purchase_manager product.model_product_product purchase.group_purchase_manager 1 1 1 1
35 access_product_pricelist_item_purchase_manager product.pricelist.item purchase_manager product.model_product_pricelist_item purchase.group_purchase_manager 1 1 1 1
36 access_account_account_purchase_manager account.account purchase manager account.model_account_account purchase.group_purchase_manager 1 0 0 0
37 access_account_journal_purchase_manager account.journal purchase manager account.model_account_journal purchase.group_purchase_manager 1 0 0 0
access_stock_location_purchase_manager stock.location purchase manager stock.model_stock_location purchase.group_purchase_manager 1 0 0 0
access_stock_warehouse_orderpoint_manager stock.warehouse.orderpoint stock.model_stock_warehouse_orderpoint purchase.group_purchase_manager 1 0 0 0
access_stock_warehouse_orderpoint_user stock.warehouse.orderpoint stock.model_stock_warehouse_orderpoint group_purchase_user 1 0 0 0
38 access_product_price_history_purchase_user prices.history purchase user product.model_product_price_history purchase.group_purchase_user 1 0 0 0
39 access_product_price_history_purchase_manager prices.history purchase manager product.model_product_price_history purchase.group_purchase_manager 1 1 1 1
40 access_purchase_bill_union access_purchase_bill_union model_purchase_bill_union purchase.group_purchase_user 1 0 0 0
@@ -43,13 +43,6 @@
<field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field>
</record>
<record model="ir.rule" id="purchase_order_report_comp_rule">
<field name="name">Purchase Order Report multi-company</field>
<field name="model_id" ref="model_purchase_report"/>
<field name="global" eval="True"/>
<field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field>
</record>
<record id="portal_purchase_order_user_rule" model="ir.rule">
<field name="name">Portal Purchase Orders</field>
<field name="model_id" ref="purchase.model_purchase_order"/>
@@ -15,31 +15,4 @@
</div>
</template>
<template id="exception_on_po">
<div class="alert alert-warning">
Exception(s) occurred on the purchase order(s):
<t t-foreach="purchase_order_ids" t-as="purchase_order">
<a href="#" data-oe-model="purchase.order" t-att-data-oe-id="purchase_order.id"><t t-esc="purchase_order.name"/></a>.
</t>
Manual actions may be needed.
<div class="mt16">
<p>Exception(s):</p>
<ul t-foreach="order_exceptions" t-as="exception">
<li>
<t t-set="order_line" t-value="exception[0]"/>
<t t-set="new_qty" t-value="exception[1][0]"/>
<t t-set="old_qty" t-value="exception[1][1]"/>
<t t-esc="new_qty"/> <t t-esc="order_line.product_uom.name"/> of <t t-esc="order_line.product_id.name"/>
ordered instead of <t t-esc="old_qty"/> <t t-esc="order_line.product_uom.name"/>
</li>
</ul>
</div>
<div class="mt16" t-if="impacted_pickings">
<p>Next transfer(s) impacted:</p>
<ul t-foreach="impacted_pickings" t-as="picking">
<li><a href="#" data-oe-model="stock.picking" t-att-data-oe-id="picking.id"><t t-esc="picking.name"/></a></li>
</ul>
</div>
</div>
</template>
</odoo>
+3 -23
View File
@@ -42,10 +42,6 @@
<!-- Purchase Control Menu -->
<menuitem id="menu_purchase_control" name="Control" parent="purchase.menu_purchase_root" sequence="4"/>
<!--Inventory control-->
<menuitem action="stock.action_receipt_picking_move" id="menu_action_picking_tree_in_move"
parent="menu_purchase_control" sequence="11"/>
<!--Invoice control-->
<menuitem
id="menu_procurement_management_pending_invoice"
@@ -142,7 +138,6 @@
<button name="button_confirm" type="object" states="sent" string="Confirm Order" class="oe_highlight" id="bid_confirm"/>
<button name="button_approve" type="object" states='to approve' string="Approve Order" class="oe_highlight" groups="purchase.group_purchase_manager"/>
<button name="action_rfq_send" states="purchase" string="Send PO by Email" type="object" context="{'send_rfq':False}"/>
<button name="action_view_picking" string="Receive Products" class="oe_highlight" type="object" attrs="{'invisible': ['|', '|' , ('is_shipped', '=', True), ('state','not in', ('purchase','done')), ('picking_count', '=', 0)]}"/>
<button name="button_draft" states="cancel" string="Set to Draft" type="object" />
<button name="button_cancel" states="draft,to approve,sent,purchase" string="Cancel" type="object" />
<button name="button_done" type="object" string="Lock" states="purchase"/>
@@ -151,13 +146,6 @@
</header>
<sheet>
<div class="oe_button_box" name="button_box">
<button type="object"
name="action_view_picking"
class="oe_stat_button"
icon="fa-truck" attrs="{'invisible':[('picking_ids','=',[])]}">
<field name="picking_count" widget="statinfo" string="Receipt" help="Incoming Shipments"/>
<field name="picking_ids" invisible="1"/>
</button>
<button type="object" name="action_view_invoice"
class="oe_stat_button"
icon="fa-pencil-square-o" attrs="{'invisible':[('state', 'in', ('draft','sent','to approve')),('invoice_ids','=',[])]}">
@@ -177,7 +165,6 @@
<field name="partner_id" context="{'search_default_supplier':1, 'default_supplier':1, 'default_customer':0}" domain="[('supplier','=',True)]"/>
<field name="partner_ref"/>
<field name="currency_id" groups="base.group_multi_currency"/>
<field name="is_shipped" invisible="1"/>
</group>
<group>
<field name="date_order"/>
@@ -195,7 +182,6 @@
<field name="sequence" widget="handle"/>
<field name="product_id" attrs="{'readonly': [('state', 'in', ('purchase', 'to approve','done', 'cancel'))]}" context="{'partner_id':parent.partner_id, 'quantity':product_qty,'uom':product_uom, 'company_id': parent.company_id}" force_save="1"/>
<field name="name"/>
<field name="move_dest_ids" invisible="1"/>
<field name="date_planned"/>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
<field name="account_analytic_id" context="{'default_partner_id':parent.partner_id}" groups="analytic.group_analytic_accounting"/>
@@ -232,9 +218,9 @@
<notebook>
<page string="Notes">
<field name="name"/>
</page><page string="Invoices and Incoming Shipments">
</page>
<page string="Invoices and Incoming Shipments">
<field name="invoice_lines"/>
<field name="move_ids"/>
</page>
</notebook>
</sheet>
@@ -251,7 +237,7 @@
<field name="notes" class="oe_inline" placeholder="Define your terms and conditions ..."/>
<div class="oe_clear"/>
</page>
<page string="Other Information">
<page string="Other Information" name="purchase_delivery_invoice">
<group>
<group>
<label for="date_planned"/>
@@ -263,10 +249,6 @@
help="This changes the scheduled date of all order lines to the given date"
class="fa fa-calendar o_icon_button oe_edit_only"/>
</div>
<field name="picking_type_id" domain="[('code','=','incoming')]" options="{'no_create': True}" groups="stock.group_stock_multi_locations"/>
<field name="dest_address_id" groups="stock.group_stock_multi_locations" attrs="{'invisible': [('default_location_dest_id_usage', '!=', 'customer')], 'required': [('default_location_dest_id_usage', '=', 'customer')]}"/>
<field name="default_location_dest_id_usage" invisible="1"/>
<field name="incoterm_id"/>
</group>
<group>
<field name="user_id"/>
@@ -485,8 +467,6 @@
<field name="name"/>
<separator string="Manual Invoices"/>
<field name="invoice_lines"/>
<separator string="Stock Moves"/>
<field name="move_ids"/>
</sheet>
</form>
</field>
@@ -13,7 +13,7 @@
<field name="company_currency_id" invisible="1"/>
<field name="po_lock" invisible="1"/>
<h2>Orders</h2>
<div class="row mt16 o_settings_container">
<div class="row mt16 o_settings_container" name="purchase_setting_container">
<div class="col-xs-12 col-md-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="po_order_approval"/>
@@ -32,20 +32,6 @@
</div>
</div>
</div>
<div class="col-xs-12 col-md-6 o_setting_box" title="Calls for tenders are used when you want to generate requests for quotations to several vendors for a given set of products. You can configure per product if you directly do a Request for Quotation to one vendor or if you want a Call for Tenders to compare offers from several vendors.">
<div class="o_setting_left_pane">
<field name="module_purchase_requisition"/>
</div>
<div class="o_setting_right_pane">
<label for="module_purchase_requisition"/>
<div class="text-muted">
Manage your purchase agreements (call for tenders, blanket orders)
</div>
<div class="content-group" attrs="{'invisible': [('module_purchase_requisition', '=', False)]}">
<div id="use_purchase_requisition"/>
</div>
</div>
</div>
<div class="col-xs-12 col-md-6 o_setting_box">
<div class="o_setting_left_pane">
<field name="lock_confirmed_po"/>
@@ -118,21 +104,6 @@
</div>
</div>
</div>
<field name="is_installed_sale" invisible="1"/>
<h2 attrs="{'invisible': [('is_installed_sale', '=', False)]}">Logistics</h2>
<div class="row mt16 o_settings_container">
<div class="col-xs-12 col-md-6 o_setting_box" attrs="{'invisible': [('is_installed_sale', '=', False)]}" title="This adds a dropshipping route to apply on products in order to request your vendors to deliver to your customers. A product to dropship will generate a purchase request for quotation once the sales order confirmed. This is a on-demand flow. The requested delivery address will be the customer delivery address and not your warehouse.">
<div class="o_setting_left_pane">
<field name="module_stock_dropshipping"/>
</div>
<div class="o_setting_right_pane">
<label for="module_stock_dropshipping"/>
<div class="text-muted">
Request your vendors to deliver to your customers
</div>
</div>
</div>
</div>
</div>
</xpath>
</field>
@@ -150,30 +121,4 @@
<menuitem id="menu_purchase_general_settings" name="Settings" parent="menu_purchase_config"
sequence="0" action="action_purchase_configuration" groups="base.group_system"/>
<record id="res_config_settings_view_form_stock" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.purchase.stock</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="stock.res_config_settings_view_form"/>
<field name="arch" type="xml">
<div id="purchase_po_lead" position="replace">
<div class="col-xs-12 col-md-6 o_setting_box" title="Margin of error for vendor lead times. When the system generates Purchase Orders for reordering products,they will be scheduled that many days earlier to cope with unexpected vendor delays.">
<div class="o_setting_left_pane">
<field name="use_po_lead"/>
</div>
<div class="o_setting_right_pane">
<label for="use_po_lead"/>
<span class="fa fa-lg fa-building-o" title="Values set here are company-specific." groups="base.group_multi_company"/>
<div class="text-muted">
Schedule receivings earlier to avoid delays
</div>
<div class="content-group">
<div class="mt16" attrs="{'invisible': [('use_po_lead','=',False)]}">
<span>Move forward expected delivery dates by <field name="po_lead" class="oe_inline"/> days</span>
</div>
</div>
</div>
</div>
</div>
</field>
</record>
</odoo>
+1 -1
View File
@@ -12,7 +12,7 @@ Manage calls for tenders and blanket orders. Calls for tenders are used to get
competing offers from different vendors and select the best ones. Blanket orders
are agreements you have with vendors to benefit from a predetermined pricing.
""",
'depends' : ['purchase'],
'depends' : ['purchase_stock'],
'demo': ['data/purchase_requisition_demo.xml'],
'data': [
'security/purchase_tender.xml',
@@ -49,7 +49,7 @@ class TestPurchaseRequisition(common.TransactionCase):
date_planned = fields.Datetime.now()
warehouse = self.env['stock.warehouse'].browse(self.ref('stock.warehouse0'))
product = self.env['product.product'].browse(self.product_13_id)
product.write({'route_ids': [(4, self.ref('purchase.route_warehouse0_buy'))]})
product.write({'route_ids': [(4, self.ref('purchase_stock.route_warehouse0_buy'))]})
self.env['procurement.group'].run(product, 14, self.env['uom.uom'].browse(self.ref('uom.product_uom_unit')), warehouse.lot_stock_id, '/', '/',
{
'warehouse_id': warehouse,
@@ -132,7 +132,7 @@ class TestPurchaseRequisition(common.TransactionCase):
# Product creation
unit = self.ref("uom.product_uom_unit")
warehouse1 = self.env.ref('stock.warehouse0')
route_buy = self.ref('purchase.route_warehouse0_buy')
route_buy = self.ref('purchase_stock.route_warehouse0_buy')
route_mto = warehouse1.mto_pull_id.route_id.id
vendor1 = self.env['res.partner'].create({'name': 'AAA', 'email': 'from.test@example.com'})
supplier_info1 = self.env['product.supplierinfo'].create({
@@ -206,7 +206,7 @@ class TestPurchaseRequisition(common.TransactionCase):
# Product creation
unit = self.ref("uom.product_uom_unit")
warehouse1 = self.env.ref('stock.warehouse0')
route_buy = self.ref('purchase.route_warehouse0_buy')
route_buy = self.ref('purchase_stock.route_warehouse0_buy')
route_mto = warehouse1.mto_pull_id.route_id.id
vendor1 = self.env['res.partner'].create({'name': 'AAA', 'email': 'from.test@example.com'})
supplier_info1 = self.env['product.supplierinfo'].create({
@@ -3,7 +3,7 @@
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.purchase.requisition</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="purchase.res_config_settings_view_form_purchase"/>
<field name="inherit_id" ref="purchase_stock.res_config_settings_view_form_purchase"/>
<field name="arch" type="xml">
<div id="use_purchase_requisition" position="replace">
<div class="mt16">
+11
View File
@@ -11,7 +11,18 @@
'website': 'https://www.odoo.com/page/purchase',
'depends': ['stock_account', 'purchase'],
'data': [
'security/ir.model.access.csv',
'security/purchase_stock_security.xml',
'data/purchase_stock_data.xml',
'data/mail_data.xml',
'views/purchase_views.xml',
'views/stock_views.xml',
'views/res_config_settings_views.xml',
'report/purchase_report_views.xml',
'report/purchase_report_templates.xml',
],
'demo': [
'data/purchase_stock_demo.xml',
],
'installable': True,
'auto_install': True,
+32
View File
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="exception_on_po">
<div class="alert alert-warning">
Exception(s) occurred on the purchase order(s):
<t t-foreach="purchase_order_ids" t-as="purchase_order">
<a href="#" data-oe-model="purchase.order" t-att-data-oe-id="purchase_order.id"><t t-esc="purchase_order.name"/></a>.
</t>
Manual actions may be needed.
<div class="mt16">
<p>Exception(s):</p>
<ul t-foreach="order_exceptions" t-as="exception">
<li>
<t t-set="order_line" t-value="exception[0]"/>
<t t-set="new_qty" t-value="exception[1][0]"/>
<t t-set="old_qty" t-value="exception[1][1]"/>
<t t-esc="new_qty"/> <t t-esc="order_line.product_uom.name"/> of <t t-esc="order_line.product_id.name"/>
ordered instead of <t t-esc="old_qty"/> <t t-esc="order_line.product_uom.name"/>
</li>
</ul>
</div>
<div class="mt16" t-if="impacted_pickings">
<p>Next transfer(s) impacted:</p>
<ul t-foreach="impacted_pickings" t-as="picking">
<li><a href="#" data-oe-model="stock.picking" t-att-data-oe-id="picking.id"><t t-esc="picking.name"/></a></li>
</ul>
</div>
</div>
</template>
</odoo>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!--
Procurement rules and routes
-->
<record id="route_warehouse0_buy" model='stock.location.route'>
<field name="name">Buy</field>
<field name="company_id"></field>
<field name="sequence">5</field>
</record>
<!-- enable purchase on main warehouse -->
<record id="stock.warehouse0" model="stock.warehouse">
<field name="buy_to_resupply" eval="True"/>
</record>
</data>
</odoo>
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="stock.res_company_1" model="res.company">
<field eval="1.0" name="po_lead"/>
</record>
<record id="product.product_delivery_01" model="product.product">
<field name="route_ids" eval="[(4,ref('route_warehouse0_buy'))]"></field>
</record>
<record id="product.product_delivery_02" model="product.product">
<field name="route_ids" eval="[(4,ref('route_warehouse0_buy'))]"></field>
</record>
<record id="product.product_product_20" model="product.product">
<field name="route_ids" eval="[(4,ref('route_warehouse0_buy'))]"></field>
</record>
</data>
<data noupdate="0">
<record id="stock.stock_warehouse_shop0" model="stock.warehouse">
<field name="buy_to_resupply" eval="True"/>
</record>
<function model="stock.warehouse" name="write">
<value model="stock.warehouse" search="[('partner_id', '=', ref('stock.res_partner_company_1'))]"/>
<value eval="{'buy_to_resupply': True}"/>
</function>
</data>
</odoo>
+4
View File
@@ -1,5 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import account_invoice
from . import procurement
from . import product
from . import purchase
from . import res_config_settings
from . import stock
@@ -0,0 +1,123 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
from odoo.tools.float_utils import float_compare
class AccountInvoice(models.Model):
_inherit = 'account.invoice'
@api.model
def invoice_line_move_line_get(self):
res = super(AccountInvoice, self).invoice_line_move_line_get()
if self.env.user.company_id.anglo_saxon_accounting:
if self.type in ['in_invoice', 'in_refund']:
for i_line in self.invoice_line_ids:
res.extend(self._anglo_saxon_purchase_move_lines(i_line, res))
return res
@api.model
def _anglo_saxon_purchase_move_lines(self, i_line, res):
"""Return the additional move lines for purchase invoices and refunds.
i_line: An account.invoice.line object.
res: The move line entries produced so far by the parent move_line_get.
"""
inv = i_line.invoice_id
company_currency = inv.company_id.currency_id
if i_line.product_id and i_line.product_id.valuation == 'real_time' and i_line.product_id.type == 'product':
# get the fiscal position
fpos = i_line.invoice_id.fiscal_position_id
# get the price difference account at the product
acc = i_line.product_id.property_account_creditor_price_difference
if not acc:
# if not found on the product get the price difference account at the category
acc = i_line.product_id.categ_id.property_account_creditor_price_difference_categ
acc = fpos.map_account(acc).id
# reference_account_id is the stock input account
reference_account_id = i_line.product_id.product_tmpl_id.get_product_accounts(fiscal_pos=fpos)['stock_input'].id
diff_res = []
# calculate and write down the possible price difference between invoice price and product price
for line in res:
if line.get('invl_id', 0) == i_line.id and reference_account_id == line['account_id']:
valuation_price_unit = i_line.product_id.uom_id._compute_price(i_line.product_id.standard_price, i_line.uom_id)
line_quantity = line['quantity']
if i_line.product_id.cost_method != 'standard' and i_line.purchase_line_id:
#for average/fifo/lifo costing method, fetch real cost price from incomming moves
valuation_price_unit = i_line.purchase_line_id.product_uom._compute_price(i_line.purchase_line_id.price_unit, i_line.uom_id)
stock_move_obj = self.env['stock.move']
valuation_stock_move = stock_move_obj.search([('purchase_line_id', '=', i_line.purchase_line_id.id), ('state', '=', 'done')])
if valuation_stock_move:
valuation_price_unit_total = 0
valuation_total_qty = 0
for val_stock_move in valuation_stock_move:
valuation_price_unit_total += abs(val_stock_move.price_unit) * val_stock_move.product_qty
valuation_total_qty += val_stock_move.product_qty
valuation_price_unit = valuation_price_unit_total / valuation_total_qty
valuation_price_unit = i_line.product_id.uom_id._compute_price(valuation_price_unit, i_line.uom_id)
line_quantity = valuation_total_qty
elif i_line.product_id.cost_method == 'real':
# In this condition, we have a real price-valuated product which has not yet been received
valuation_price_unit = i_line.purchase_line_id.price_unit
interim_account_price = valuation_price_unit * line_quantity
if inv.currency_id.id != company_currency.id:
# We express everyhting in the invoice currency
valuation_price_unit = company_currency._convert(valuation_price_unit, inv.currency_id, inv.company_id, inv.date_invoice or fields.Date.today(), round=False)
interim_account_price = company_currency._convert(interim_account_price, inv.currency_id, inv.company_id, inv.date_invoice or fields.Date.today(), round=False)
invoice_cur_prec = inv.currency_id.decimal_places
if float_compare(valuation_price_unit, i_line.price_unit, precision_digits=invoice_cur_prec) != 0 and float_compare(line['price_unit'], i_line.price_unit, precision_digits=invoice_cur_prec) == 0:
# price with discount and without tax included
price_unit = i_line.price_unit * (1 - (i_line.discount or 0.0) / 100.0)
tax_ids = []
if line['tax_ids']:
#line['tax_ids'] is like [(4, tax_id, None), (4, tax_id2, None)...]
taxes = self.env['account.tax'].browse([x[1] for x in line['tax_ids']])
price_unit = taxes.compute_all(price_unit, currency=inv.currency_id, quantity=1.0)['total_excluded']
for tax in taxes:
tax_ids.append((4, tax.id, None))
for child in tax.children_tax_ids:
if child.type_tax_use != 'none':
tax_ids.append((4, child.id, None))
price_before = line.get('price', 0.0)
price_unit_val_dif = price_unit - valuation_price_unit
price_val_dif = price_before - interim_account_price
if inv.currency_id.compare_amounts(i_line.price_unit, i_line.purchase_line_id.price_unit) != 0 and acc:
# If the unit prices have not changed and we have a
# valuation difference, it means this difference is due to exchange rates,
# so we don't create anything, the exchange rate entries will
# be processed automatically by the rest of the code.
diff_res.append({
'type': 'src',
'name': i_line.name[:64],
'price_unit': inv.currency_id.round(price_unit_val_dif),
'quantity': line_quantity,
'price': inv.currency_id.round(price_val_dif),
'account_id': acc,
'product_id': line['product_id'],
'uom_id': line['uom_id'],
'account_analytic_id': line['account_analytic_id'],
'tax_ids': tax_ids,
})
return diff_res
return []
def _get_last_step_stock_moves(self):
""" Overridden from stock_account.
Returns the stock moves associated to this invoice."""
rslt = super(AccountInvoice, self)._get_last_step_stock_moves()
for invoice in self.filtered(lambda x: x.type == 'in_invoice'):
rslt += invoice.mapped('invoice_line_ids.purchase_line_id.move_ids').filtered(lambda x: x.state == 'done' and x.location_id.usage == 'supplier')
for invoice in self.filtered(lambda x: x.type == 'in_refund'):
rslt += invoice.mapped('invoice_line_ids.purchase_line_id.move_ids').filtered(lambda x: x.state == 'done' and x.location_dest_id.usage == 'supplier')
return rslt
+18
View File
@@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class ProductTemplate(models.Model):
_name = 'product.template'
_inherit = 'product.template'
@api.model
def _get_buy_route(self):
buy_route = self.env.ref('purchase_stock.route_warehouse0_buy', raise_if_not_found=False)
if buy_route:
return buy_route.ids
return []
route_ids = fields.Many2many(default=lambda self: self._get_buy_route())
+369
View File
@@ -0,0 +1,369 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.tools.float_utils import float_compare
from odoo.exceptions import UserError
from odoo.addons.purchase.models.purchase import PurchaseOrder as Purchase
class PurchaseOrder(models.Model):
_inherit = 'purchase.order'
@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]
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.")
picking_count = fields.Integer(compute='_compute_picking', string='Picking count', default=0, store=True)
picking_ids = fields.Many2many('stock.picking', compute='_compute_picking', string='Receptions', copy=False, store=True)
picking_type_id = fields.Many2one('stock.picking.type', 'Deliver To', states=Purchase.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.depends('order_line.move_ids.returned_move_ids',
'order_line.move_ids.state',
'order_line.move_ids.picking_id')
def _compute_picking(self):
for order in self:
pickings = self.env['stock.picking']
for line in order.order_line:
# We keep a limited scope on purpose. Ideally, we should also use move_orig_ids and
# do some recursive search, but that could be prohibitive if not done correctly.
moves = line.move_ids | line.move_ids.mapped('returned_move_ids')
pickings |= moves.mapped('picking_id')
order.picking_ids = pickings
order.picking_count = len(pickings)
@api.depends('picking_ids', 'picking_ids.state')
def _compute_is_shipped(self):
for order in self:
if order.picking_ids and all([x.state == 'done' for x in order.picking_ids]):
order.is_shipped = True
@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
# --------------------------------------------------
# CRUD
# --------------------------------------------------
def write(self, vals):
if vals.get('order_line') and self.state == 'purchase':
for order in self:
pre_order_line_qty = {order_line: order_line.product_qty for order_line in order.mapped('order_line')}
res = super(PurchaseOrder, self).write(vals)
if vals.get('order_line') and self.state == 'purchase':
for order in self:
to_log = {}
for order_line in order.order_line:
if pre_order_line_qty.get(order_line, False) and float_compare(pre_order_line_qty[order_line], order_line.product_qty, precision_rounding=order_line.product_uom.rounding) > 0:
to_log[order_line] = (order_line.product_qty, pre_order_line_qty[order_line])
if to_log:
order._log_decrease_ordered_quantity(to_log)
return res
# --------------------------------------------------
# Actions
# --------------------------------------------------
@api.multi
def button_approve(self, force=False):
result = super(PurchaseOrder, self).button_approve(force=force)
self._create_picking()
return result
@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))
# If the product is MTO, change the procure_method of the the closest move to purchase to MTS.
# The purpose is to link the po that the user will manually generate to the existing moves's chain.
if order.state in ('draft', 'sent', 'to approve'):
for order_line in order.order_line:
if order_line.move_dest_ids:
siblings_states = (order_line.move_dest_ids.mapped('move_orig_ids')).mapped('state')
if all(state in ('done', 'cancel') for state in siblings_states):
order_line.move_dest_ids.write({'procure_method': 'make_to_stock'})
order_line.move_dest_ids._recompute_state()
for pick in order.picking_ids.filtered(lambda r: r.state != 'cancel'):
pick.action_cancel()
return super(PurchaseOrder, self).button_cancel()
@api.multi
def action_view_picking(self):
""" This function returns an action that display existing picking orders of given purchase order ids. When only one found, show the picking immediately.
"""
action = self.env.ref('stock.action_picking_tree')
result = action.read()[0]
# override the context to get rid of the default filtering on operation type
result['context'] = {}
pick_ids = self.mapped('picking_ids')
# choose the view_mode accordingly
if len(pick_ids) > 1:
result['domain'] = "[('id','in',%s)]" % (pick_ids.ids)
elif len(pick_ids) == 1:
res = self.env.ref('stock.view_picking_form', False)
result['views'] = [(res and res.id or False, 'form')]
result['res_id'] = pick_ids.id
return result
# --------------------------------------------------
# Business methods
# --------------------------------------------------
def _log_decrease_ordered_quantity(self, purchase_order_lines_quantities):
def _keys_in_sorted(move):
""" sort by picking and the responsible for the product the
move.
"""
return (move.picking_id.id, move.product_id.responsible_id.id)
def _keys_in_groupby(move):
""" group by picking and the responsible for the product the
move.
"""
return (move.picking_id, move.product_id.responsible_id)
def _render_note_exception_quantity_po(order_exceptions):
order_line_ids = self.env['purchase.order.line'].browse([order_line.id for order in order_exceptions.values() for order_line in order[0]])
purchase_order_ids = order_line_ids.mapped('order_id')
move_ids = self.env['stock.move'].concat(*rendering_context.keys())
impacted_pickings = move_ids.mapped('picking_id')._get_impacted_pickings(move_ids) - move_ids.mapped('picking_id')
values = {
'purchase_order_ids': purchase_order_ids,
'order_exceptions': order_exceptions.values(),
'impacted_pickings': impacted_pickings,
}
return self.env.ref('purchase_stock.exception_on_po').render(values=values)
documents = self.env['stock.picking']._log_activity_get_documents(purchase_order_lines_quantities, 'move_ids', 'DOWN', _keys_in_sorted, _keys_in_groupby)
filtered_documents = {}
for (parent, responsible), rendering_context in documents.items():
if parent._name == 'stock.picking':
if parent.state == 'cancel':
continue
filtered_documents[(parent, responsible)] = rendering_context
self.env['stock.picking']._log_activity(_render_note_exception_quantity_po, filtered_documents)
@api.multi
def _get_destination_location(self):
self.ensure_one()
if self.dest_address_id:
return self.dest_address_id.property_stock_customer.id
return self.picking_type_id.default_location_dest_id.id
@api.model
def _prepare_picking(self):
if not self.group_id:
self.group_id = self.group_id.create({
'name': self.name,
'partner_id': self.partner_id.id
})
if not self.partner_id.property_stock_supplier.id:
raise UserError(_("You must set a Vendor Location for this partner %s") % self.partner_id.name)
return {
'picking_type_id': self.picking_type_id.id,
'partner_id': self.partner_id.id,
'date': self.date_order,
'origin': self.name,
'location_dest_id': self._get_destination_location(),
'location_id': self.partner_id.property_stock_supplier.id,
'company_id': self.company_id.id,
}
@api.multi
def _create_picking(self):
StockPicking = self.env['stock.picking']
for order in self:
if any([ptype in ['product', 'consu'] for ptype in order.order_line.mapped('product_id.type')]):
pickings = order.picking_ids.filtered(lambda x: x.state not in ('done', 'cancel'))
if not pickings:
res = order._prepare_picking()
picking = StockPicking.create(res)
else:
picking = pickings[0]
moves = order.order_line._create_stock_moves(picking)
moves = moves.filtered(lambda x: x.state not in ('done', 'cancel'))._action_confirm()
seq = 0
for move in sorted(moves, key=lambda move: move.date_expected):
seq += 5
move.sequence = seq
moves._action_assign()
picking.message_post_with_view('mail.message_origin_link',
values={'self': picking, 'origin': order},
subtype_id=self.env.ref('mail.mt_note').id)
return True
class PurchaseOrderLine(models.Model):
_inherit = 'purchase.order.line'
move_ids = fields.One2many('stock.move', 'purchase_line_id', string='Reservation', readonly=True, ondelete='set null', copy=False)
orderpoint_id = fields.Many2one('stock.warehouse.orderpoint', 'Orderpoint')
move_dest_ids = fields.One2many('stock.move', 'created_purchase_line_id', 'Downstream Moves')
@api.model
def create(self, values):
line = super(PurchaseOrderLine, self).create(values)
if line.order_id.state == 'purchase':
line._create_or_update_picking()
return line
@api.multi
def write(self, values):
result = super(PurchaseOrderLine, self).write(values)
# Update expected date of corresponding moves
if 'date_planned' in values:
self.env['stock.move'].search([
('purchase_line_id', 'in', self.ids), ('state', '!=', 'done')
]).write({'date_expected': values['date_planned']})
if 'product_qty' in values:
self.filtered(lambda l: l.order_id.state == 'purchase')._create_or_update_picking()
return result
# --------------------------------------------------
# Business methods
# --------------------------------------------------
@api.multi
def _create_or_update_picking(self):
for line in self:
if line.product_id.type in ('product', 'consu'):
# Prevent decreasing below received quantity
if float_compare(line.product_qty, line.qty_received, line.product_uom.rounding) < 0:
raise UserError(_('You cannot decrease the ordered quantity below the received quantity.\n'
'Create a return first.'))
if float_compare(line.product_qty, line.qty_invoiced, line.product_uom.rounding) == -1:
# If the quantity is now below the invoiced quantity, create an activity on the vendor bill
# inviting the user to create a refund.
activity = self.env['mail.activity'].sudo().create({
'activity_type_id': self.env.ref('mail.mail_activity_data_todo').id,
'note': _('The quantities on your purchase order indicate less than billed. You should ask for a refund. '),
'res_id': line.invoice_lines[0].invoice_id.id,
'res_model_id': self.env.ref('account.model_account_invoice').id,
})
activity._onchange_activity_type_id()
# If the user increased quantity of existing line or created a new line
pickings = line.order_id.picking_ids.filtered(lambda x: x.state not in ('done', 'cancel') and x.location_dest_id.usage in ('internal', 'transit'))
picking = pickings and pickings[0] or False
if not picking:
res = line.order_id._prepare_picking()
picking = self.env['stock.picking'].create(res)
move_vals = line._prepare_stock_moves(picking)
for move_val in move_vals:
self.env['stock.move']\
.create(move_val)\
._action_confirm()\
._action_assign()
@api.multi
def _get_stock_move_price_unit(self):
self.ensure_one()
line = self[0]
order = line.order_id
price_unit = line.price_unit
if line.taxes_id:
price_unit = line.taxes_id.with_context(round=False).compute_all(
price_unit, currency=line.order_id.currency_id, quantity=1.0, product=line.product_id, partner=line.order_id.partner_id
)['total_excluded']
if line.product_uom.id != line.product_id.uom_id.id:
price_unit *= line.product_uom.factor / line.product_id.uom_id.factor
if order.currency_id != order.company_id.currency_id:
price_unit = order.currency_id._convert(
price_unit, order.company_id.currency_id, self.company_id, self.date_order or fields.Date.today(), 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_uom._compute_quantity(move.product_uom_qty, self.product_uom, rounding_method='HALF-UP')
template = {
'name': self.name or '',
'product_id': self.product_id.id,
'product_uom': self.product_uom.id,
'date': self.order_id.date_order,
'date_expected': self.date_planned,
'location_id': self.order_id.partner_id.property_stock_supplier.id,
'location_dest_id': self.order_id._get_destination_location(),
'picking_id': picking.id,
'partner_id': self.order_id.dest_address_id.id,
'move_dest_ids': [(4, x) for x in self.move_dest_ids.ids],
'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,
'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,
}
diff_quantity = self.product_qty - qty
if float_compare(diff_quantity, 0.0, precision_rounding=self.product_uom.rounding) > 0:
quant_uom = self.product_id.uom_id
get_param = self.env['ir.config_parameter'].sudo().get_param
if self.product_uom.id != quant_uom.id and get_param('stock.propagate_uom') != '1':
product_qty = self.product_uom._compute_quantity(diff_quantity, quant_uom, rounding_method='HALF-UP')
template['product_uom'] = quant_uom.id
template['product_uom_qty'] = product_qty
else:
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
def _update_received_qty(self):
for line in self:
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
def _merge_in_existing_line(self, product_id, product_qty, product_uom, location_id, name, origin, values):
""" This function purpose is to be override with the purpose to forbide _run_buy method
to merge a new po line in an existing one.
"""
return True
@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class ResConfigSettings(models.TransientModel):
_inherit = 'res.config.settings'
module_purchase_requisition = fields.Boolean("Purchase Agreements")
module_stock_dropshipping = fields.Boolean("Dropshipping")
is_installed_sale = fields.Boolean(string="Is the Sale Module Installed")
@api.multi
def get_values(self):
res = super(ResConfigSettings, self).get_values()
res.update(
is_installed_sale=self.env['ir.module.module'].search([('name', '=', 'sale'), ('state', '=', 'installed')]).id,
)
return res
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import purchase_report
@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class PurchaseReport(models.Model):
_inherit = "purchase.report"
picking_type_id = fields.Many2one('stock.warehouse', 'Warehouse', readonly=True)
def _select(self):
return super(PurchaseReport, self)._select() + ", spt.warehouse_id as picking_type_id"
def _from(self):
return super(PurchaseReport, self)._from() + " left join stock_picking_type spt on (spt.id=s.picking_type_id)"
def _group_by(self):
return super(PurchaseReport, self)._group_by() + ", spt.warehouse_id"
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="report_purchaseorder_document" inherit_id="purchase.report_purchaseorder_document">
<xpath expr="//div[@name='purchase_shipping_address']" position="inside">
<div t-if="not o.dest_address_id and o.picking_type_id and o.picking_type_id.warehouse_id">
<span t-field="o.picking_type_id.warehouse_id.name"/>
<div t-field="o.picking_type_id.warehouse_id.partner_id" t-options='{"widget": "contact", "fields": ["address", "phone"], "no_marker": True, "phone_icons": True}'/>
</div>
</xpath>
</template>
<template id="report_purchasequotation_document" inherit_id="purchase.report_purchasequotation_document">
<xpath expr="//div[@name='purchase_shipping_address']" position="inside">
<div t-if="not o.dest_address_id and o.picking_type_id.warehouse_id">
<span t-field="o.picking_type_id.warehouse_id.name"/>
<div t-field="o.picking_type_id.warehouse_id.partner_id" t-options='{"widget": "contact", "fields": ["address", "phone"], "no_marker": True, "phone_icons": True}'/>
</div>
</xpath>
</template>
</odoo>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="purchase_report_view_search" model="ir.ui.view">
<field name="name">purchase.report.search.stock</field>
<field name="model">purchase.report</field>
<field name="inherit_id" ref="purchase.view_purchase_order_search"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='user_id']" position="after">
<field name="picking_type_id"/>
</xpath>
</field>
</record>
</odoo>
@@ -0,0 +1,16 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_purchase_order_stock_worker,purchase.order,model_purchase_order,stock.group_stock_user,1,0,0,0
access_purchase_order_line_stock_worker,purchase.order.line,model_purchase_order_line,stock.group_stock_user,1,0,0,0
access_stock_location_purchase_user,stock.location,stock.model_stock_location,purchase.group_purchase_user,1,0,0,0
access_stock_warehouse_purchase_user,stock.warehouse,stock.model_stock_warehouse,purchase.group_purchase_user,1,0,0,0
access_stock_picking_purchase_user,stock.picking,stock.model_stock_picking,purchase.group_purchase_user,1,1,1,1
access_stock_move_purchase_user,stock.move,stock.model_stock_move,purchase.group_purchase_user,1,1,1,0
access_stock_location_purchase_user_manager,stock.location,stock.model_stock_location,purchase.group_purchase_manager,1,0,0,0
access_stock_warehouse_purchase_user_manager,stock.warehouse,stock.model_stock_warehouse,purchase.group_purchase_manager,1,0,0,0
access_stock_picking_purchase_user_manager,stock.picking,stock.model_stock_picking,purchase.group_purchase_manager,1,1,1,1
access_stock_move_purchase_user_manager,stock.move,stock.model_stock_move,purchase.group_purchase_manager,1,1,1,1
access_stock_location_purchase_manager,stock.location purchase manager,stock.model_stock_location,purchase.group_purchase_manager,1,0,0,0
access_stock_warehouse_orderpoint_manager,stock.warehouse.orderpoint,stock.model_stock_warehouse_orderpoint,purchase.group_purchase_manager,1,0,0,0
access_stock_warehouse_orderpoint_user,stock.warehouse.orderpoint,stock.model_stock_warehouse_orderpoint,purchase.group_purchase_user,1,0,0,0
access_report_purchase_order,purchase.stock.report,model_purchase_report,purchase.group_purchase_manager,1,0,0,0
access_report_purchase_order_user,purchase.stock.report user,model_purchase_report,purchase.group_purchase_user,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_purchase_order_stock_worker purchase.order model_purchase_order stock.group_stock_user 1 0 0 0
3 access_purchase_order_line_stock_worker purchase.order.line model_purchase_order_line stock.group_stock_user 1 0 0 0
4 access_stock_location_purchase_user stock.location stock.model_stock_location purchase.group_purchase_user 1 0 0 0
5 access_stock_warehouse_purchase_user stock.warehouse stock.model_stock_warehouse purchase.group_purchase_user 1 0 0 0
6 access_stock_picking_purchase_user stock.picking stock.model_stock_picking purchase.group_purchase_user 1 1 1 1
7 access_stock_move_purchase_user stock.move stock.model_stock_move purchase.group_purchase_user 1 1 1 0
8 access_stock_location_purchase_user_manager stock.location stock.model_stock_location purchase.group_purchase_manager 1 0 0 0
9 access_stock_warehouse_purchase_user_manager stock.warehouse stock.model_stock_warehouse purchase.group_purchase_manager 1 0 0 0
10 access_stock_picking_purchase_user_manager stock.picking stock.model_stock_picking purchase.group_purchase_manager 1 1 1 1
11 access_stock_move_purchase_user_manager stock.move stock.model_stock_move purchase.group_purchase_manager 1 1 1 1
12 access_stock_location_purchase_manager stock.location purchase manager stock.model_stock_location purchase.group_purchase_manager 1 0 0 0
13 access_stock_warehouse_orderpoint_manager stock.warehouse.orderpoint stock.model_stock_warehouse_orderpoint purchase.group_purchase_manager 1 0 0 0
14 access_stock_warehouse_orderpoint_user stock.warehouse.orderpoint stock.model_stock_warehouse_orderpoint purchase.group_purchase_user 1 0 0 0
15 access_report_purchase_order purchase.stock.report model_purchase_report purchase.group_purchase_manager 1 0 0 0
16 access_report_purchase_order_user purchase.stock.report user model_purchase_report purchase.group_purchase_user 1 0 0 0
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="purchase_order_report_comp_rule" model="ir.rule">
<field name="name">Purchase Order Report multi-company</field>
<field name="model_id" ref="model_purchase_report"/>
<field name="global" eval="True"/>
<field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field>
</record>
</data>
</odoo>
@@ -57,7 +57,7 @@ class TestCreatePicking(common.TestProductCommon):
ml.qty_done = ml.product_uom_qty
self.picking.action_done()
self.assertEqual(self.po.order_line.mapped('qty_received'), [7.0], 'Purchase: all products should be received')
# create new order line
self.po.write({'order_line': [
@@ -115,7 +115,7 @@ class TestCreatePicking(common.TestProductCommon):
product = self.env['product.product'].create({
'name': 'product',
'type': 'product',
'route_ids': [(4, self.ref('stock.route_warehouse0_mto')), (4, self.ref('purchase.route_warehouse0_buy'))],
'route_ids': [(4, self.ref('stock.route_warehouse0_mto')), (4, self.ref('purchase_stock.route_warehouse0_buy'))],
'seller_ids': [(6, 0, [seller.id])],
'categ_id': self.env.ref('product.product_category_all').id,
})
@@ -247,7 +247,7 @@ class TestCreatePicking(common.TestProductCommon):
product = self.env['product.product'].create({
'name': 'product',
'type': 'product',
'route_ids': [(4, self.ref('stock.route_warehouse0_mto')), (4, self.ref('purchase.route_warehouse0_buy'))],
'route_ids': [(4, self.ref('stock.route_warehouse0_mto')), (4, self.ref('purchase_stock.route_warehouse0_buy'))],
'seller_ids': [(6, 0, [seller.id])],
'categ_id': self.env.ref('product.product_category_all').id,
})
@@ -10,34 +10,34 @@ class TestReplenishWizard(TestStockCommon):
super(TestReplenishWizard, self).setUp()
self.vendor = self.env['res.partner'].search([('supplier', '=', True)], limit=1)
self.product1_price = 500
# Create a supplier info witch the previous vendor
self.supplierinfo = self.env['product.supplierinfo'].create({
'name': self.vendor.id,
'price': self.product1_price,
})
# Create a product with the 'buy' route and
# Create a product with the 'buy' route and
# the 'supplierinfo' prevously created
self.product1 = self.env['product.product'].create({
'name': 'product a',
'type': 'product',
'categ_id': self.env.ref('product.product_category_all').id,
'seller_ids': [(4, self.supplierinfo.id, 0)],
'route_ids': [(4, self.env.ref('purchase.route_warehouse0_buy').id, 0)],
'route_ids': [(4, self.env.ref('purchase_stock.route_warehouse0_buy').id, 0)],
})
# Additional Values required by the replenish wizard
self.uom_unit = self.env.ref('uom.product_uom_unit')
self.wh = self.env['stock.warehouse'].search([('company_id', '=', self.env.user.id)], limit=1)
def test_replenish_buy_1(self):
""" Set a quantity to replenish via the "Buy" route and check if
a purchase order is created with the correct values
a purchase order is created with the correct values
"""
self.product_uom_qty = 42
replenish_wizard = self.env['product.replenish'].create({
'product_id': self.product1.id,
'product_tmpl_id': self.product1.product_tmpl_id.id,
@@ -45,7 +45,7 @@ class TestReplenishWizard(TestStockCommon):
'quantity': self.product_uom_qty,
'warehouse_id': self.wh.id,
})
replenish_wizard.launch_replenishment()
last_po_id = self.env['purchase.order'].search([
('origin', '=', 'Manual Replenishment'),
@@ -56,4 +56,4 @@ class TestReplenishWizard(TestStockCommon):
self.assertTrue(order_line,'The product is not in the Purchase Order')
self.assertEqual(order_line.product_qty, self.product_uom_qty, 'Quantities does not match')
self.assertEqual(order_line.price_unit, self.product1_price, 'Prices does not match')
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="purchase_order_view_form_inherit" model="ir.ui.view">
<field name="name">purchase.order.form.inherit</field>
<field name="model">purchase.order</field>
<field name="inherit_id" ref="purchase.purchase_order_form"/>
<field name="arch" type="xml">
<xpath expr="//header/button[@name='action_rfq_send']" position="after">
<button name="action_view_picking" string="Receive Products" class="oe_highlight" type="object" attrs="{'invisible': ['|', '|' , ('is_shipped', '=', True), ('state','not in', ('purchase','done')), ('picking_count', '=', 0)]}"/>
</xpath>
<xpath expr="//div[hasclass('oe_button_box')]" position="inside">
<button type="object"
name="action_view_picking"
class="oe_stat_button"
icon="fa-truck" attrs="{'invisible':[('picking_ids','=',[])]}">
<field name="picking_count" widget="statinfo" string="Receipt" help="Incoming Shipments"/>
<field name="picking_ids" invisible="1"/>
</button>
</xpath>
<xpath expr="//field[@name='currency_id']" position="after">
<field name="is_shipped" invisible="1"/>
</xpath>
<xpath expr="//field[@name='order_line']/tree//field[@name='date_planned']" position="after">
<field name="move_dest_ids" invisible="1"/>
</xpath>
<xpath expr="//field[@name='order_line']/form//field[@name='invoice_lines']" position="after">
<field name="move_ids"/>
</xpath>
<xpath expr="//page[@name='purchase_delivery_invoice']/group" position="inside">
<field name="picking_type_id" domain="[('code','=','incoming')]" options="{'no_create': True}" groups="stock.group_stock_multi_locations"/>
<field name="dest_address_id" groups="stock.group_stock_multi_locations" attrs="{'invisible': [('default_location_dest_id_usage', '!=', 'customer')], 'required': [('default_location_dest_id_usage', '=', 'customer')]}"/>
<field name="default_location_dest_id_usage" invisible="1"/>
<field name="incoterm_id"/>
</xpath>
</field>
</record>
<record id="purchase_order_line_view_form_inherit" model="ir.ui.view">
<field name="name">purchase.order.line.form.inherit</field>
<field name="model">purchase.order.line</field>
<field name="inherit_id" ref="purchase.purchase_order_line_form2"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='invoice_lines']" position="after">
<separator string="Stock Moves"/>
<field name="move_ids"/>
</xpath>
</field>
</record>
</odoo>
@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="res_config_settings_view_form_purchase" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.purchase</field>
<field name="model">res.config.settings</field>
<field name="priority" eval="25"/>
<field name="inherit_id" ref="purchase.res_config_settings_view_form_purchase"/>
<field name="arch" type="xml">
<xpath expr="//div[hasclass('app_settings_block')]" position="inside">
<field name="is_installed_sale" invisible="1"/>
<h2 attrs="{'invisible': [('is_installed_sale', '=', False)]}">Logistics</h2>
<div class="row mt16 o_settings_container">
<div class="col-xs-12 col-md-6 o_setting_box" attrs="{'invisible': [('is_installed_sale', '=', False)]}" title="This adds a dropshipping route to apply on products in order to request your vendors to deliver to your customers. A product to dropship will generate a purchase request for quotation once the sales order confirmed. This is a on-demand flow. The requested delivery address will be the customer delivery address and not your warehouse.">
<div class="o_setting_left_pane">
<field name="module_stock_dropshipping"/>
</div>
<div class="o_setting_right_pane">
<label for="module_stock_dropshipping"/>
<div class="text-muted">
Request your vendors to deliver to your customers
</div>
</div>
</div>
</div>
</xpath>
<xpath expr="//div[@name='purchase_setting_container']" position="inside">
<div class="col-xs-12 col-md-6 o_setting_box" title="Calls for tenders are used when you want to generate requests for quotations to several vendors for a given set of products. You can configure per product if you directly do a Request for Quotation to one vendor or if you want a Call for Tenders to compare offers from several vendors.">
<div class="o_setting_left_pane">
<field name="module_purchase_requisition"/>
</div>
<div class="o_setting_right_pane">
<label for="module_purchase_requisition"/>
<div class="text-muted">
Manage your purchase agreements (call for tenders, blanket orders)
</div>
<div class="content-group" attrs="{'invisible': [('module_purchase_requisition', '=', False)]}">
<div id="use_purchase_requisition"/>
</div>
</div>
</div>
</xpath>
</field>
</record>
<record id="res_config_settings_view_form_stock" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.purchase.stock</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="stock.res_config_settings_view_form"/>
<field name="arch" type="xml">
<div id="purchase_po_lead" position="replace">
<div class="col-xs-12 col-md-6 o_setting_box" title="Margin of error for vendor lead times. When the system generates Purchase Orders for reordering products,they will be scheduled that many days earlier to cope with unexpected vendor delays.">
<div class="o_setting_left_pane">
<field name="use_po_lead"/>
</div>
<div class="o_setting_right_pane">
<label for="use_po_lead"/>
<span class="fa fa-lg fa-building-o" title="Values set here are company-specific." groups="base.group_multi_company"/>
<div class="text-muted">
Schedule receivings earlier to avoid delays
</div>
<div class="content-group">
<div class="mt16" attrs="{'invisible': [('use_po_lead','=',False)]}">
<span>Move forward expected delivery dates by <field name="po_lead" class="oe_inline"/> days</span>
</div>
</div>
</div>
</div>
</div>
</field>
</record>
</odoo>
@@ -29,4 +29,11 @@
<field name="view_mode">tree,form</field>
<field name="domain">[('purchase_id', '=', active_id)]</field>
</record>
<!-- Inventory control -->
<menuitem
id="menu_action_picking_tree_in_move"
action="stock.action_receipt_picking_move"
parent="purchase.menu_purchase_control"
sequence="11"/>
</odoo>
+1 -1
View File
@@ -21,7 +21,7 @@ internal transfer document is needed.
""",
'website': 'https://www.odoo.com/page/warehouse',
'depends': ['purchase', 'sale_stock'],
'depends': ['purchase_stock', 'sale_stock'],
'data': ['data/stock_data.xml'],
'installable': True,
'auto_install': False,
+1 -1
View File
@@ -11,7 +11,7 @@ Landed Costs Management
This module allows you to easily add extra costs on pickings and decide the split of these costs among their stock moves in order to take them into account in your stock valuation.
""",
'website': 'https://www.odoo.com/page/warehouse',
'depends': ['stock_account', 'purchase'],
'depends': ['stock_account', 'purchase_stock'],
'category': 'Warehouse',
'sequence': 16,
'demo': [
+2 -2
View File
@@ -8,9 +8,9 @@
This module will test the main workflow of Odoo.
It will install some main apps and will try to execute the most important actions.
""",
'depends': ['web_tour', 'crm', 'sale_timesheet', 'purchase', 'mrp', 'account'],
'depends': ['web_tour', 'crm', 'sale_timesheet', 'purchase_stock', 'mrp', 'account'],
'data': [
'views/templates.xml',
],
],
'installable': True,
}