[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:
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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())
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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': [
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user