Skip to content

Commit 9d4efc8

Browse files
committed
[IMP] purchase: Editable PO + minor fixes
1 parent cbe66ef commit 9d4efc8

File tree

7 files changed

+178
-39
lines changed

7 files changed

+178
-39
lines changed

addons/purchase/company.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,17 @@ class company(models.Model):
1212
"they will be scheduled that many days earlier "
1313
"to cope with unexpected vendor delays.", default=0.0)
1414

15+
po_lock = fields.Selection([
16+
('edit', 'Allow to edit purchase orders'),
17+
('lock', 'Confirmed purchase orders are not editable')
18+
], string="Purchase Order Modification", default="edit",
19+
help='Purchase Order Modification used when you want to purchase order editable after confirm')
20+
1521
po_double_validation = fields.Selection([
1622
('one_step', 'Confirm purchase orders in one step'),
1723
('two_step', 'Get 2 levels of approvals to confirm a purchase order')
1824
], string="Levels of Approvals", default='one_step',\
1925
help="Provide a double validation mechanism for purchases")
26+
2027
po_double_validation_amount = fields.Monetary(string='Double validation amount', default=5000,\
2128
help="Minimum amount for which a double validation is required")

addons/purchase/company_view.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<field name="arch" type="xml">
1111
<xpath expr="//group[@name='logistics_grp']" position="inside">
1212
<field name="po_lead"/>
13+
<field name="po_lock" widget="radio"/>
1314
<field name="po_double_validation" widget="radio"/>
1415
<field name="po_double_validation_amount" attrs="{'invisible': [('po_double_validation', '=', 'one_step')]}"/>
1516
</xpath>

addons/purchase/purchase.py

Lines changed: 63 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def _compute_date_planned(self):
4545
def _get_invoiced(self):
4646
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
4747
for order in self:
48-
if order.state != 'purchase':
48+
if order.state not in ('purchase', 'done'):
4949
order.invoice_status = 'no'
5050
continue
5151

@@ -122,16 +122,16 @@ def _compute_is_shipped(self):
122122
('done', 'Locked'),
123123
('cancel', 'Cancelled')
124124
], string='Status', readonly=True, select=True, copy=False, default='draft', track_visibility='onchange')
125-
order_line = fields.One2many('purchase.order.line', 'order_id', string='Order Lines', states=READONLY_STATES, copy=True)
125+
order_line = fields.One2many('purchase.order.line', 'order_id', string='Order Lines', states={'cancel': [('readonly', True)], 'done': [('readonly', True)]}, copy=True)
126126
notes = fields.Text('Terms and Conditions')
127127

128-
invoice_count = fields.Integer(compute="_compute_invoice", string='# of Invoices', copy=False, default=0)
129-
invoice_ids = fields.Many2many('account.invoice', compute="_compute_invoice", string='Invoices', copy=False)
128+
invoice_count = fields.Integer(compute="_compute_invoice", string='# of Bills', copy=False, default=0)
129+
invoice_ids = fields.Many2many('account.invoice', compute="_compute_invoice", string='Bills', copy=False)
130130
invoice_status = fields.Selection([
131-
('no', 'Not purchased'),
132-
('to invoice', 'Waiting Invoices'),
133-
('invoiced', 'Invoice Received'),
134-
], string='Invoice Status', compute='_get_invoiced', store=True, readonly=True, copy=False, default='no')
131+
('no', 'Nothing to Bill'),
132+
('to invoice', 'Waiting Bills'),
133+
('invoiced', 'Bills Received'),
134+
], string='Billing Status', compute='_get_invoiced', store=True, readonly=True, copy=False, default='no')
135135

136136
picking_count = fields.Integer(compute='_compute_picking', string='Receptions', default=0)
137137
picking_ids = fields.Many2many('stock.picking', compute='_compute_picking', string='Receptions', copy=False)
@@ -145,7 +145,7 @@ def _compute_is_shipped(self):
145145

146146
fiscal_position_id = fields.Many2one('account.fiscal.position', string='Fiscal Position', oldname='fiscal_position')
147147
payment_term_id = fields.Many2one('account.payment.term', 'Payment Term')
148-
incoterm_id = fields.Many2one('stock.incoterms', 'Incoterm', help="International Commercial Terms are a series of predefined commercial terms used in international transactions.")
148+
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.")
149149

150150
product_id = fields.Many2one('product.product', related='order_line.product_id', string='Product')
151151
create_uid = fields.Many2one('res.users', 'Responsible')
@@ -304,9 +304,15 @@ def print_quotation(self):
304304
return self.env['report'].get_action(self, 'purchase.report_purchasequotation')
305305

306306
@api.multi
307-
def button_approve(self):
307+
def button_approve(self, force=False):
308+
if self.company_id.po_double_validation == 'two_step'\
309+
and self.amount_total >= self.env.user.company_id.currency_id.compute(self.company_id.po_double_validation_amount, self.currency_id)\
310+
and not self.user_has_groups('purchase.group_purchase_manager'):
311+
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))
308312
self.write({'state': 'purchase'})
309313
self._create_picking()
314+
if self.company_id.po_lock == 'lock':
315+
self.write({'state': 'done'})
310316
return {}
311317

312318
@api.multi
@@ -319,14 +325,11 @@ def button_confirm(self):
319325
for order in self:
320326
order._add_supplier_to_product()
321327
# Deal with double validation process
322-
if order.company_id.po_double_validation == 'one_step'\
323-
or (order.company_id.po_double_validation == 'two_step'\
324-
and order.amount_total < self.env.user.company_id.currency_id.compute(order.company_id.po_double_validation_amount, order.currency_id))\
325-
or order.user_has_groups('purchase.group_purchase_manager'):
326-
order.button_approve()
328+
if order.company_id.po_double_validation == 'one_step':
329+
order.button_approve(force=True)
327330
else:
328331
order.write({'state': 'to approve'})
329-
return {}
332+
return True
330333

331334
@api.multi
332335
def button_cancel(self):
@@ -381,13 +384,18 @@ def _prepare_picking(self):
381384

382385
@api.multi
383386
def _create_picking(self):
387+
StockPicking = self.env['stock.picking']
384388
for order in self:
385389
if any([ptype in ['product', 'consu'] for ptype in order.order_line.mapped('product_id.type')]):
386-
res = order._prepare_picking()
387-
picking = self.env['stock.picking'].create(res)
388-
moves = order.order_line.filtered(lambda r: r.product_id.type in ['product', 'consu'])._create_stock_moves(picking)
389-
moves.action_confirm()
390-
order.order_line.mapped('move_ids').force_assign()
390+
pickings = order.picking_ids.filtered(lambda x: x.state not in ('done','cancel'))
391+
if not pickings:
392+
res = order._prepare_picking()
393+
picking = StockPicking.create(res)
394+
else:
395+
picking = pickings[0]
396+
moves = order.order_line._create_stock_moves(picking)
397+
moves = moves.action_confirm()
398+
moves.force_assign()
391399
picking.message_post_with_view('mail.message_origin_link',
392400
values={'self': picking, 'origin': order},
393401
subtype_id=self.env.ref('mail.mt_note').id)
@@ -523,7 +531,22 @@ def _compute_qty_received(self):
523531
total += move.product_uom_qty
524532
line.qty_received = total
525533

534+
@api.model
535+
def create(self, values):
536+
line = super(PurchaseOrderLine, self).create(values)
537+
if line.order_id.state == 'purchase':
538+
line.order_id._create_picking()
539+
return line
540+
541+
@api.multi
542+
def write(self, values):
543+
result = super(PurchaseOrderLine, self).write(values)
544+
orders = self.filtered(lambda x: x.order_id.state == 'purchase').mapped('order_id')
545+
orders._create_picking()
546+
return result
547+
526548
name = fields.Text(string='Description', required=True)
549+
sequence = fields.Integer(string='Sequence', default=10)
527550
product_qty = fields.Float(string='Quantity', digits=dp.get_precision('Product Unit of Measure'), required=True)
528551
date_planned = fields.Datetime(string='Scheduled Date', required=True, select=True)
529552
taxes_id = fields.Many2many('account.tax', string='Taxes', domain=['|', ('active', '=', False), ('active', '=', True)])
@@ -542,7 +565,7 @@ def _compute_qty_received(self):
542565
company_id = fields.Many2one('res.company', related='order_id.company_id', string='Company', store=True, readonly=True)
543566
state = fields.Selection(related='order_id.state', store=True)
544567

545-
invoice_lines = fields.One2many('account.invoice.line', 'purchase_line_id', string="Invoice Lines", readonly=True, copy=False)
568+
invoice_lines = fields.One2many('account.invoice.line', 'purchase_line_id', string="Bill Lines", readonly=True, copy=False)
546569

547570
# Replace by invoiced Qty
548571
qty_invoiced = fields.Float(compute='_compute_qty_invoiced', string="Billed Qty", store=True)
@@ -572,8 +595,12 @@ def _create_stock_moves(self, picking):
572595
moves = self.env['stock.move']
573596
done = self.env['stock.move'].browse()
574597
for line in self:
598+
if line.product_id.type not in ['product', 'consu']:
599+
continue
600+
qty = 0.0
575601
price_unit = line._get_stock_move_price_unit()
576-
602+
for move in line.move_ids.filtered(lambda x: x.state != 'cancel'):
603+
qty += move.product_qty
577604
template = {
578605
'name': line.name or '',
579606
'product_id': line.product_id.id,
@@ -596,9 +623,8 @@ def _create_stock_moves(self, picking):
596623
'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 [],
597624
'warehouse_id':line.order_id.picking_type_id.warehouse_id.id,
598625
}
599-
600626
# Fullfill all related procurements with this po line
601-
diff_quantity = line.product_qty
627+
diff_quantity = line.product_qty - qty
602628
for procurement in line.procurement_ids:
603629
procurement_qty = procurement.product_uom._compute_quantity(procurement.product_qty, line.product_uom)
604630
tmp = template.copy()
@@ -618,7 +644,7 @@ def _create_stock_moves(self, picking):
618644
@api.multi
619645
def unlink(self):
620646
for line in self:
621-
if line.order_id.state in ['approved', 'done']:
647+
if line.order_id.state in ['purchase', 'done']:
622648
raise UserError(_('Cannot delete a purchase order line which is in state \'%s\'.') %(line.state,))
623649
for proc in line.procurement_ids:
624650
proc.message_post(body=_('Purchase order line deleted.'))
@@ -722,6 +748,15 @@ def _onchange_quantity(self):
722748

723749
self.price_unit = price_unit
724750

751+
@api.onchange('product_qty')
752+
def _onchange_product_qty(self):
753+
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:
754+
warning_mess = {
755+
'title': _('Ordered quantity decreased!'),
756+
'message' : _('You are decreasing the ordered quantity!\nYou must update the quantities on the reception and/or bills.'),
757+
}
758+
return {'warning': warning_mess}
759+
725760
def _suggest_quantity(self):
726761
'''
727762
Suggest a minimal quantity based on the seller
@@ -991,8 +1026,8 @@ def _purchase_count(self):
9911026
('purchase', 'On ordered quantities'),
9921027
('receive', 'On received quantities'),
9931028
], string="Control Purchase Bills",
994-
help="On ordered quantities: Invoice this product based on ordered quantities.\n"
995-
"On received quantities: Invoice this product based on received quantity.", default="receive")
1029+
help="On ordered quantities: control bills based on ordered quantities.\n"
1030+
"On received quantities: control bills based on received quantity.", default="receive")
9961031
route_ids = fields.Many2many(default=lambda self: self._get_buy_route())
9971032
purchase_line_warn = fields.Selection(WARNING_MESSAGE, 'Purchase Order Line', help=WARNING_HELP, required=True, default="no-message")
9981033
purchase_line_warn_msg = fields.Text('Message for Purchase Order Line')

addons/purchase/purchase_view.xml

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -175,21 +175,23 @@
175175
<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)]}"/>
176176
<button name="button_draft" states="cancel" string="Set to Draft" type="object" />
177177
<button name="button_cancel" states="draft,to approve,sent,purchase" string="Cancel" type="object" />
178-
<button name="button_done" type="object" string="Lock Bills" states="purchase"/>
178+
<button name="button_done" type="object" string="Lock" states="purchase"/>
179179
<field name="state" widget="statusbar" statusbar_visible="draft,sent,purchase" readonly="1"/>
180180
</header>
181181
<sheet>
182-
<div class="oe_button_box" name="button_box" attrs="{'invisible': [('state', 'not in', ('purchase', 'done', 'cancel'))]}">
182+
<div class="oe_button_box" name="button_box">
183183
<button type="object"
184184
name="action_view_picking"
185185
class="oe_stat_button"
186-
icon="fa-truck">
186+
icon="fa-truck" attrs="{'invisible':[('state', 'in', ('draft','sent','to approve')),('picking_ids','=',[])]}">
187187
<field name="picking_count" widget="statinfo" string="Shipment" help="Incoming Shipments"/>
188+
<field name="picking_ids" invisible="1"/>
188189
</button>
189190
<button type="object" name="action_view_invoice"
190191
class="oe_stat_button"
191-
icon="fa-pencil-square-o">
192+
icon="fa-pencil-square-o" attrs="{'invisible':[('state', 'in', ('draft','sent','to approve')),('invoice_ids','=',[])]}">
192193
<field name="invoice_count" widget="statinfo" string="Invoices"/>
194+
<field name='invoice_ids' invisible="1"/>
193195
</button>
194196
</div>
195197
<div class="oe_title">
@@ -214,9 +216,11 @@
214216
</group>
215217
<notebook>
216218
<page string="Products">
217-
<field name="order_line">
218-
<tree string="Purchase Order Lines" editable="bottom">
219-
<field name="product_id" context="{'partner_id': parent.partner_id}"/>
219+
<field name="order_line" attrs="{'readonly': [('state', 'in', ('done', 'cancel'))]}">
220+
<tree string="Purchase Order Lines" editable="bottom">
221+
<field name="state" invisible="1"/>
222+
<field name="sequence" widget="handle"/>
223+
<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}"/>
220224
<field name="name"/>
221225
<field name="date_planned"/>
222226
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
@@ -225,7 +229,7 @@
225229
<field name="product_qty"/>
226230
<field name="qty_received" invisible="not context.get('show_purchase', False)"/>
227231
<field name="qty_invoiced" invisible="not context.get('show_purchase', False)"/>
228-
<field name="product_uom" groups="product.group_uom"/>
232+
<field name="product_uom" groups="product.group_uom" attrs="{'readonly': [('state', 'in', ('purchase', 'done', 'cancel'))]}"/>
229233
<field name="price_unit"/>
230234
<field name="taxes_id" widget="many2many_tags" domain="[('type_tax_use','=','purchase')]" context="{'default_type_tax_use': 'purchase'}"/>
231235
<field name="price_subtotal" widget="monetary"/>
@@ -292,8 +296,8 @@
292296
</group>
293297
<group>
294298
<field name="invoice_status"/>
295-
<field name="payment_term_id" options="{'no_open': True, 'no_create': True}" attrs="{'readonly': [('invoice_status','=', 'invoiced')]}"/>
296-
<field name="fiscal_position_id" attrs="{'readonly': [('invoice_status','=', 'invoiced')]}" />
299+
<field name="payment_term_id" options="{'no_open': True, 'no_create': True}" attrs="{'readonly': ['|', ('invoice_status','=', 'invoiced'), ('state', '=', 'done')]}"/>
300+
<field name="fiscal_position_id" attrs="{'readonly': ['|', ('invoice_status','=', 'invoiced'), ('state', '=', 'done')]}"/>
297301
<field name="date_approve" groups="base.group_no_one"/>
298302
</group>
299303
</group>

addons/purchase/res_config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# -*- coding: utf-8 -*-
22
# Part of Odoo. See LICENSE file for full copyright and licensing details.
33

4+
from openerp import SUPERUSER_ID
45
from openerp.osv import fields, osv
56
from openerp.tools.translate import _
67

addons/purchase/tests/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# -*- coding: utf-8 -*-
22
# Part of Odoo. See LICENSE file for full copyright and licensing details.
33

4-
from . import test_onchange_product_id, test_purchase_order
4+
from . import test_onchange_product_id, test_purchase_order, test_create_picking

0 commit comments

Comments
 (0)