[IMP] purchase: Editable PO + minor fixes

This commit is contained in:
Fabien Pinckaers
2016-08-02 18:10:48 -07:00
parent cbe66ef236
commit 9d4efc81a6
7 changed files with 178 additions and 39 deletions
+7
View File
@@ -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")
+1
View File
@@ -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
View File
@@ -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')
+14 -10
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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".')