[IMP] purchase: Editable PO + minor fixes
This commit is contained in:
@@ -12,10 +12,17 @@ class company(models.Model):
|
||||
"they will be scheduled that many days earlier "
|
||||
"to cope with unexpected vendor delays.", default=0.0)
|
||||
|
||||
po_lock = fields.Selection([
|
||||
('edit', 'Allow to edit purchase orders'),
|
||||
('lock', 'Confirmed purchase orders are not editable')
|
||||
], string="Purchase Order Modification", default="edit",
|
||||
help='Purchase Order Modification used when you want to purchase order editable after confirm')
|
||||
|
||||
po_double_validation = fields.Selection([
|
||||
('one_step', 'Confirm purchase orders in one step'),
|
||||
('two_step', 'Get 2 levels of approvals to confirm a purchase order')
|
||||
], string="Levels of Approvals", default='one_step',\
|
||||
help="Provide a double validation mechanism for purchases")
|
||||
|
||||
po_double_validation_amount = fields.Monetary(string='Double validation amount', default=5000,\
|
||||
help="Minimum amount for which a double validation is required")
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//group[@name='logistics_grp']" position="inside">
|
||||
<field name="po_lead"/>
|
||||
<field name="po_lock" widget="radio"/>
|
||||
<field name="po_double_validation" widget="radio"/>
|
||||
<field name="po_double_validation_amount" attrs="{'invisible': [('po_double_validation', '=', 'one_step')]}"/>
|
||||
</xpath>
|
||||
|
||||
+63
-28
@@ -45,7 +45,7 @@ class PurchaseOrder(models.Model):
|
||||
def _get_invoiced(self):
|
||||
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
for order in self:
|
||||
if order.state != 'purchase':
|
||||
if order.state not in ('purchase', 'done'):
|
||||
order.invoice_status = 'no'
|
||||
continue
|
||||
|
||||
@@ -122,16 +122,16 @@ class PurchaseOrder(models.Model):
|
||||
('done', 'Locked'),
|
||||
('cancel', 'Cancelled')
|
||||
], string='Status', readonly=True, select=True, copy=False, default='draft', track_visibility='onchange')
|
||||
order_line = fields.One2many('purchase.order.line', 'order_id', string='Order Lines', states=READONLY_STATES, copy=True)
|
||||
order_line = fields.One2many('purchase.order.line', 'order_id', string='Order Lines', states={'cancel': [('readonly', True)], 'done': [('readonly', True)]}, copy=True)
|
||||
notes = fields.Text('Terms and Conditions')
|
||||
|
||||
invoice_count = fields.Integer(compute="_compute_invoice", string='# of Invoices', copy=False, default=0)
|
||||
invoice_ids = fields.Many2many('account.invoice', compute="_compute_invoice", string='Invoices', copy=False)
|
||||
invoice_count = fields.Integer(compute="_compute_invoice", string='# of Bills', copy=False, default=0)
|
||||
invoice_ids = fields.Many2many('account.invoice', compute="_compute_invoice", string='Bills', copy=False)
|
||||
invoice_status = fields.Selection([
|
||||
('no', 'Not purchased'),
|
||||
('to invoice', 'Waiting Invoices'),
|
||||
('invoiced', 'Invoice Received'),
|
||||
], string='Invoice Status', compute='_get_invoiced', store=True, readonly=True, copy=False, default='no')
|
||||
('no', 'Nothing to Bill'),
|
||||
('to invoice', 'Waiting Bills'),
|
||||
('invoiced', 'Bills Received'),
|
||||
], string='Billing Status', compute='_get_invoiced', store=True, readonly=True, copy=False, default='no')
|
||||
|
||||
picking_count = fields.Integer(compute='_compute_picking', string='Receptions', default=0)
|
||||
picking_ids = fields.Many2many('stock.picking', compute='_compute_picking', string='Receptions', copy=False)
|
||||
@@ -145,7 +145,7 @@ 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 Term')
|
||||
incoterm_id = fields.Many2one('stock.incoterms', 'Incoterm', help="International Commercial Terms are a series of predefined commercial terms used in international transactions.")
|
||||
incoterm_id = fields.Many2one('stock.incoterms', 'Incoterm', states={'done': [('readonly', True)]}, help="International Commercial Terms are a series of predefined commercial terms used in international transactions.")
|
||||
|
||||
product_id = fields.Many2one('product.product', related='order_line.product_id', string='Product')
|
||||
create_uid = fields.Many2one('res.users', 'Responsible')
|
||||
@@ -304,9 +304,15 @@ class PurchaseOrder(models.Model):
|
||||
return self.env['report'].get_action(self, 'purchase.report_purchasequotation')
|
||||
|
||||
@api.multi
|
||||
def button_approve(self):
|
||||
def button_approve(self, force=False):
|
||||
if self.company_id.po_double_validation == 'two_step'\
|
||||
and self.amount_total >= self.env.user.company_id.currency_id.compute(self.company_id.po_double_validation_amount, self.currency_id)\
|
||||
and not self.user_has_groups('purchase.group_purchase_manager'):
|
||||
raise UserError(_('You need purchase manager access rights to validate an order above %.2f %s.') % (self.company_id.po_double_validation_amount, self.company_id.currency_id.name))
|
||||
self.write({'state': 'purchase'})
|
||||
self._create_picking()
|
||||
if self.company_id.po_lock == 'lock':
|
||||
self.write({'state': 'done'})
|
||||
return {}
|
||||
|
||||
@api.multi
|
||||
@@ -319,14 +325,11 @@ class PurchaseOrder(models.Model):
|
||||
for order in self:
|
||||
order._add_supplier_to_product()
|
||||
# Deal with double validation process
|
||||
if order.company_id.po_double_validation == 'one_step'\
|
||||
or (order.company_id.po_double_validation == 'two_step'\
|
||||
and order.amount_total < self.env.user.company_id.currency_id.compute(order.company_id.po_double_validation_amount, order.currency_id))\
|
||||
or order.user_has_groups('purchase.group_purchase_manager'):
|
||||
order.button_approve()
|
||||
if order.company_id.po_double_validation == 'one_step':
|
||||
order.button_approve(force=True)
|
||||
else:
|
||||
order.write({'state': 'to approve'})
|
||||
return {}
|
||||
return True
|
||||
|
||||
@api.multi
|
||||
def button_cancel(self):
|
||||
@@ -381,13 +384,18 @@ class PurchaseOrder(models.Model):
|
||||
|
||||
@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')]):
|
||||
res = order._prepare_picking()
|
||||
picking = self.env['stock.picking'].create(res)
|
||||
moves = order.order_line.filtered(lambda r: r.product_id.type in ['product', 'consu'])._create_stock_moves(picking)
|
||||
moves.action_confirm()
|
||||
order.order_line.mapped('move_ids').force_assign()
|
||||
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.action_confirm()
|
||||
moves.force_assign()
|
||||
picking.message_post_with_view('mail.message_origin_link',
|
||||
values={'self': picking, 'origin': order},
|
||||
subtype_id=self.env.ref('mail.mt_note').id)
|
||||
@@ -523,7 +531,22 @@ class PurchaseOrderLine(models.Model):
|
||||
total += move.product_uom_qty
|
||||
line.qty_received = total
|
||||
|
||||
@api.model
|
||||
def create(self, values):
|
||||
line = super(PurchaseOrderLine, self).create(values)
|
||||
if line.order_id.state == 'purchase':
|
||||
line.order_id._create_picking()
|
||||
return line
|
||||
|
||||
@api.multi
|
||||
def write(self, values):
|
||||
result = super(PurchaseOrderLine, self).write(values)
|
||||
orders = self.filtered(lambda x: x.order_id.state == 'purchase').mapped('order_id')
|
||||
orders._create_picking()
|
||||
return result
|
||||
|
||||
name = fields.Text(string='Description', required=True)
|
||||
sequence = fields.Integer(string='Sequence', default=10)
|
||||
product_qty = fields.Float(string='Quantity', digits=dp.get_precision('Product Unit of Measure'), required=True)
|
||||
date_planned = fields.Datetime(string='Scheduled Date', required=True, select=True)
|
||||
taxes_id = fields.Many2many('account.tax', string='Taxes', domain=['|', ('active', '=', False), ('active', '=', True)])
|
||||
@@ -542,7 +565,7 @@ class PurchaseOrderLine(models.Model):
|
||||
company_id = fields.Many2one('res.company', related='order_id.company_id', string='Company', store=True, readonly=True)
|
||||
state = fields.Selection(related='order_id.state', store=True)
|
||||
|
||||
invoice_lines = fields.One2many('account.invoice.line', 'purchase_line_id', string="Invoice Lines", readonly=True, copy=False)
|
||||
invoice_lines = fields.One2many('account.invoice.line', 'purchase_line_id', string="Bill Lines", readonly=True, copy=False)
|
||||
|
||||
# Replace by invoiced Qty
|
||||
qty_invoiced = fields.Float(compute='_compute_qty_invoiced', string="Billed Qty", store=True)
|
||||
@@ -572,8 +595,12 @@ class PurchaseOrderLine(models.Model):
|
||||
moves = self.env['stock.move']
|
||||
done = self.env['stock.move'].browse()
|
||||
for line in self:
|
||||
if line.product_id.type not in ['product', 'consu']:
|
||||
continue
|
||||
qty = 0.0
|
||||
price_unit = line._get_stock_move_price_unit()
|
||||
|
||||
for move in line.move_ids.filtered(lambda x: x.state != 'cancel'):
|
||||
qty += move.product_qty
|
||||
template = {
|
||||
'name': line.name or '',
|
||||
'product_id': line.product_id.id,
|
||||
@@ -596,9 +623,8 @@ class PurchaseOrderLine(models.Model):
|
||||
'route_ids': line.order_id.picking_type_id.warehouse_id and [(6, 0, [x.id for x in line.order_id.picking_type_id.warehouse_id.route_ids])] or [],
|
||||
'warehouse_id':line.order_id.picking_type_id.warehouse_id.id,
|
||||
}
|
||||
|
||||
# Fullfill all related procurements with this po line
|
||||
diff_quantity = line.product_qty
|
||||
diff_quantity = line.product_qty - qty
|
||||
for procurement in line.procurement_ids:
|
||||
procurement_qty = procurement.product_uom._compute_quantity(procurement.product_qty, line.product_uom)
|
||||
tmp = template.copy()
|
||||
@@ -618,7 +644,7 @@ class PurchaseOrderLine(models.Model):
|
||||
@api.multi
|
||||
def unlink(self):
|
||||
for line in self:
|
||||
if line.order_id.state in ['approved', 'done']:
|
||||
if line.order_id.state in ['purchase', 'done']:
|
||||
raise UserError(_('Cannot delete a purchase order line which is in state \'%s\'.') %(line.state,))
|
||||
for proc in line.procurement_ids:
|
||||
proc.message_post(body=_('Purchase order line deleted.'))
|
||||
@@ -722,6 +748,15 @@ class PurchaseOrderLine(models.Model):
|
||||
|
||||
self.price_unit = price_unit
|
||||
|
||||
@api.onchange('product_qty')
|
||||
def _onchange_product_qty(self):
|
||||
if (self.state == 'purchase' or self.state == 'to approve') and self.product_id.type in ['product', 'consu'] and self.product_qty < self._origin.product_qty:
|
||||
warning_mess = {
|
||||
'title': _('Ordered quantity decreased!'),
|
||||
'message' : _('You are decreasing the ordered quantity!\nYou must update the quantities on the reception and/or bills.'),
|
||||
}
|
||||
return {'warning': warning_mess}
|
||||
|
||||
def _suggest_quantity(self):
|
||||
'''
|
||||
Suggest a minimal quantity based on the seller
|
||||
@@ -991,8 +1026,8 @@ class ProductTemplate(models.Model):
|
||||
('purchase', 'On ordered quantities'),
|
||||
('receive', 'On received quantities'),
|
||||
], string="Control Purchase Bills",
|
||||
help="On ordered quantities: Invoice this product based on ordered quantities.\n"
|
||||
"On received quantities: Invoice this product based on received quantity.", default="receive")
|
||||
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')
|
||||
|
||||
@@ -175,21 +175,23 @@
|
||||
<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 Bills" states="purchase"/>
|
||||
<button name="button_done" type="object" string="Lock" states="purchase"/>
|
||||
<field name="state" widget="statusbar" statusbar_visible="draft,sent,purchase" readonly="1"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<div class="oe_button_box" name="button_box" attrs="{'invisible': [('state', 'not in', ('purchase', 'done', 'cancel'))]}">
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button type="object"
|
||||
name="action_view_picking"
|
||||
class="oe_stat_button"
|
||||
icon="fa-truck">
|
||||
icon="fa-truck" attrs="{'invisible':[('state', 'in', ('draft','sent','to approve')),('picking_ids','=',[])]}">
|
||||
<field name="picking_count" widget="statinfo" string="Shipment" 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">
|
||||
icon="fa-pencil-square-o" attrs="{'invisible':[('state', 'in', ('draft','sent','to approve')),('invoice_ids','=',[])]}">
|
||||
<field name="invoice_count" widget="statinfo" string="Invoices"/>
|
||||
<field name='invoice_ids' invisible="1"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
@@ -214,9 +216,11 @@
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Products">
|
||||
<field name="order_line">
|
||||
<tree string="Purchase Order Lines" editable="bottom">
|
||||
<field name="product_id" context="{'partner_id': parent.partner_id}"/>
|
||||
<field name="order_line" attrs="{'readonly': [('state', 'in', ('done', 'cancel'))]}">
|
||||
<tree string="Purchase Order Lines" editable="bottom">
|
||||
<field name="state" invisible="1"/>
|
||||
<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}"/>
|
||||
<field name="name"/>
|
||||
<field name="date_planned"/>
|
||||
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
|
||||
@@ -225,7 +229,7 @@
|
||||
<field name="product_qty"/>
|
||||
<field name="qty_received" invisible="not context.get('show_purchase', False)"/>
|
||||
<field name="qty_invoiced" invisible="not context.get('show_purchase', False)"/>
|
||||
<field name="product_uom" groups="product.group_uom"/>
|
||||
<field name="product_uom" groups="product.group_uom" attrs="{'readonly': [('state', 'in', ('purchase', 'done', 'cancel'))]}"/>
|
||||
<field name="price_unit"/>
|
||||
<field name="taxes_id" widget="many2many_tags" domain="[('type_tax_use','=','purchase')]" context="{'default_type_tax_use': 'purchase'}"/>
|
||||
<field name="price_subtotal" widget="monetary"/>
|
||||
@@ -292,8 +296,8 @@
|
||||
</group>
|
||||
<group>
|
||||
<field name="invoice_status"/>
|
||||
<field name="payment_term_id" options="{'no_open': True, 'no_create': True}" attrs="{'readonly': [('invoice_status','=', 'invoiced')]}"/>
|
||||
<field name="fiscal_position_id" attrs="{'readonly': [('invoice_status','=', 'invoiced')]}" />
|
||||
<field name="payment_term_id" options="{'no_open': True, 'no_create': True}" attrs="{'readonly': ['|', ('invoice_status','=', 'invoiced'), ('state', '=', 'done')]}"/>
|
||||
<field name="fiscal_position_id" attrs="{'readonly': ['|', ('invoice_status','=', 'invoiced'), ('state', '=', 'done')]}"/>
|
||||
<field name="date_approve" groups="base.group_no_one"/>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from openerp import SUPERUSER_ID
|
||||
from openerp.osv import fields, osv
|
||||
from openerp.tools.translate import _
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import test_onchange_product_id, test_purchase_order
|
||||
from . import test_onchange_product_id, test_purchase_order, test_create_picking
|
||||
@@ -0,0 +1,91 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import datetime
|
||||
from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT
|
||||
from odoo.addons.product.tests import common
|
||||
|
||||
|
||||
class TestCreatePicking(common.TestProductCommon):
|
||||
|
||||
def setUp(self):
|
||||
super(TestCreatePicking, self).setUp()
|
||||
self.partner_id = self.env.ref('base.res_partner_1')
|
||||
self.product_id_1 = self.env.ref('product.product_product_8')
|
||||
self.product_id_2 = self.env.ref('product.product_product_11')
|
||||
res_users_purchase_user = self.env.ref('purchase.group_purchase_user')
|
||||
|
||||
Users = self.env['res.users'].with_context({'no_reset_password': True, 'mail_create_nosubscribe': True})
|
||||
self.user_purchase_user = Users.create({
|
||||
'name': 'Pauline Poivraisselle',
|
||||
'login': 'pauline',
|
||||
'email': 'pur@example.com',
|
||||
'notify_email': 'none',
|
||||
'groups_id': [(6, 0, [res_users_purchase_user.id])]})
|
||||
|
||||
self.po_vals = {
|
||||
'partner_id': self.partner_id.id,
|
||||
'order_line': [
|
||||
(0, 0, {
|
||||
'name': self.product_id_1.name,
|
||||
'product_id': self.product_id_1.id,
|
||||
'product_qty': 5.0,
|
||||
'product_uom': self.product_id_1.uom_po_id.id,
|
||||
'price_unit': 500.0,
|
||||
'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
|
||||
})],
|
||||
}
|
||||
|
||||
def test_00_create_picking(self):
|
||||
|
||||
# Draft purchase order created
|
||||
self.po = self.env['purchase.order'].create(self.po_vals)
|
||||
self.assertTrue(self.po, 'Purchase: no purchase order created')
|
||||
|
||||
# Purchase order confirm
|
||||
self.po.button_confirm()
|
||||
self.assertEqual(self.po.state, 'purchase', 'Purchase: PO state should be "Purchase')
|
||||
self.assertEqual(self.po.picking_count, 1, 'Purchase: one picking should be created')
|
||||
self.assertEqual(len(self.po.order_line.move_ids), 1, 'One move should be created')
|
||||
|
||||
# Change purchase order line product quantity
|
||||
self.po.order_line.write({'product_qty': 7.0})
|
||||
self.assertEqual(len(self.po.order_line.move_ids), 2, 'Two move should be created')
|
||||
|
||||
# Validate first shipment
|
||||
self.picking = self.po.picking_ids[0]
|
||||
self.picking.force_assign()
|
||||
self.picking.pack_operation_product_ids.write({'qty_done': 7.0})
|
||||
self.picking.do_new_transfer()
|
||||
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': [
|
||||
(0, 0, {
|
||||
'name': self.product_id_2.name,
|
||||
'product_id': self.product_id_2.id,
|
||||
'product_qty': 5.0,
|
||||
'product_uom': self.product_id_2.uom_po_id.id,
|
||||
'price_unit': 250.0,
|
||||
'date_planned': datetime.today().strftime(DEFAULT_SERVER_DATETIME_FORMAT),
|
||||
})]})
|
||||
self.assertEqual(self.po.picking_count, 2, 'New picking should be created')
|
||||
moves = self.po.order_line.mapped('move_ids').filtered(lambda x: x.state not in ('done', 'cancel'))
|
||||
self.assertEqual(len(moves), 1, 'One move should be created')
|
||||
|
||||
def test_01_check_double_validation(self):
|
||||
|
||||
# make double validation two step
|
||||
self.env.user.company_id.write({'po_double_validation': 'two_step'})
|
||||
|
||||
# Draft purchase order created
|
||||
self.po = self.env['purchase.order'].sudo(self.user_purchase_user).create(self.po_vals)
|
||||
self.assertTrue(self.po, 'Purchase: no purchase order created')
|
||||
|
||||
# Purchase order confirm
|
||||
self.po.button_confirm()
|
||||
self.assertEqual(self.po.state, 'to approve', 'Purchase: PO state should be "to approve".')
|
||||
|
||||
# PO approved by manager
|
||||
self.po.button_approve()
|
||||
self.assertEqual(self.po.state, 'purchase', 'PO state should be "Purchase".')
|
||||
Reference in New Issue
Block a user