[MERGE] stock submodules: migrate to new API

* Move files and reorganize directories according to guidelines
* Lint and clean code, remove dead code
* Migrate code to the new API

Applied to stock submodules: stock_account, stock_calendar,
stock_landed_costs.

The following modules were already in new API, but cleaned and reorganized:
stock_dropshipping, stock_picking_wave, sale_stock

Two fixes from saas-12 in yaml_import have been included in this branch
because originally developped when fixing issues related to the new API
migration.
This commit is contained in:
Thibault Delavallée
2016-07-13 10:00:45 +02:00
87 changed files with 2164 additions and 2432 deletions
+1
View File
@@ -1218,6 +1218,7 @@ class product_product(osv.osv):
'company_id': company_id,
}, context=context)
@api.cr_uid_ids_context
def get_history_price(self, cr, uid, product_id, company_id, date=None, context=None):
if context is None:
context = {}
+4 -29
View File
@@ -806,45 +806,20 @@ class ProcurementOrder(models.Model):
return all(move.state == 'done' for move in self.move_ids)
return super(ProcurementOrder, self)._check()
@api.v8
def _get_purchase_schedule_date(self):
"""Return the datetime value to use as Schedule Date (``date_planned``) for the
Purchase Order Lines created to satisfy the given procurement. """
procurement_date_planned = datetime.strptime(self.date_planned, DEFAULT_SERVER_DATETIME_FORMAT)
schedule_date = (procurement_date_planned - relativedelta(days=self.company_id.po_lead))
return schedule_date
@api.v7
def _get_purchase_schedule_date(self, procurement):
"""Return the datetime value to use as Schedule Date (``date_planned``) for the
Purchase Order Lines created to satisfy the given procurement.
:param browse_record procurement: the procurement for which a PO will be created.
:rtype: datetime
:return: the desired Schedule Date for the PO lines
"""
procurement_date_planned = datetime.strptime(procurement.date_planned, DEFAULT_SERVER_DATETIME_FORMAT)
schedule_date = (procurement_date_planned - relativedelta(days=procurement.company_id.po_lead))
return schedule_date
@api.v8
def _get_purchase_order_date(self, schedule_date):
"""Return the datetime value to use as Order Date (``date_order``) for the
Purchase Order created to satisfy the given procurement. """
self.ensure_one()
seller_delay = int(self.product_id._select_seller(self.product_id).delay)
return schedule_date - relativedelta(days=seller_delay)
@api.v7
def _get_purchase_order_date(self, cr, uid, procurement, company, schedule_date, context=None):
"""Return the datetime value to use as Order Date (``date_order``) for the
Purchase Order created to satisfy the given procurement.
:param browse_record procurement: the procurement for which a PO will be created.
:param browse_report company: the company to which the new PO will belong to.
:param datetime schedule_date: desired Scheduled Date for the Purchase Order lines.
:rtype: datetime
:return: the desired Order Date for the PO
"""
seller_delay = int(procurement.product_id._select_seller(procurement.product_id).delay)
return schedule_date - relativedelta(days=seller_delay)
@api.multi
def _prepare_purchase_order_line(self, po, supplier):
self.ensure_one()
+2 -4
View File
@@ -1,7 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import sale_stock
import report
import company
import res_config
from . import models
from . import report
+8 -7
View File
@@ -29,14 +29,15 @@ You can choose flexible invoicing methods:
'data': [
'security/sale_stock_security.xml',
'security/ir.model.access.csv',
'company_view.xml',
'sale_stock_view.xml',
'stock_view.xml',
'res_config_view.xml',
'account_invoice_view.xml',
'views/report_deliveryslip.xml',
'views/res_company_views.xml',
'views/sale_order_views.xml',
'views/stock_views.xml',
'views/sale_config_settings_views.xml',
'views/account_invoice_views.xml',
'report/sale_order_report_templates.xml',
'report/stock_report_deliveryslip.xml',
],
'demo': ['sale_stock_demo.xml'],
'demo': ['data/sale_order_demo.xml'],
'installable': True,
'auto_install': True,
}
+10
View File
@@ -0,0 +1,10 @@
# -*- 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 res_company
from . import sale_order
from . import sale_config_settings
from . import stock
@@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class AccountInvoice(models.Model):
_inherit = 'account.invoice'
incoterms_id = fields.Many2one('stock.incoterms', string="Incoterms",
help="Incoterms are series of sales terms. They are used to divide transaction costs and responsibilities between buyer and seller and reflect state-of-the-art transportation practices.",
readonly=True, states={'draft': [('readonly', False)]})
class AccountInvoiceLine(models.Model):
_inherit = "account.invoice.line"
def _get_anglo_saxon_price_unit(self):
price_unit = super(AccountInvoiceLine,self)._get_anglo_saxon_price_unit()
# in case of anglo saxon with a product configured as invoiced based on delivery, with perpetual
# valuation and real price costing method, we must find the real price for the cost of good sold
uom_obj = self.env['product.uom']
if self.product_id.invoice_policy == "delivery":
for s_line in self.sale_line_ids:
# qtys already invoiced
qty_done = sum([uom_obj._compute_qty_obj(x.uom_id, x.quantity, x.product_id.uom_id) for x in s_line.invoice_lines if x.invoice_id.state in ('open', 'paid')])
quantity = uom_obj._compute_qty_obj(self.uom_id, self.quantity, self.product_id.uom_id)
# Put moves in fixed order by date executed
moves = self.env['stock.move']
for procurement in s_line.procurement_ids:
moves |= procurement.move_ids
moves.sorted(lambda x: x.date)
# Go through all the moves and do nothing until you get to qty_done
# Beyond qty_done we need to calculate the average of the price_unit
# on the moves we encounter.
average_price_unit = self._compute_average_price(qty_done, quantity, moves)
price_unit = average_price_unit or price_unit
price_unit = uom_obj._compute_qty_obj(self.uom_id, price_unit, self.product_id.uom_id, round=False)
return price_unit
def _compute_average_price(self, qty_done, quantity, moves):
average_price_unit = 0
qty_delivered = 0
invoiced_qty = 0
for move in moves:
if move.state != 'done':
continue
invoiced_qty += move.product_qty
if invoiced_qty <= qty_done:
continue
qty_to_consider = move.product_qty
if invoiced_qty - move.product_qty < qty_done:
qty_to_consider = invoiced_qty - qty_done
qty_to_consider = min(qty_to_consider, quantity - qty_delivered)
qty_delivered += qty_to_consider
average_price_unit = (average_price_unit * (qty_delivered - qty_to_consider) + move.price_unit * qty_to_consider) / qty_delivered
if qty_delivered == quantity:
break
return average_price_unit
+14
View File
@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class ProcurementOrder(models.Model):
_inherit = "procurement.order"
def _run_move_create(self):
vals = super(ProcurementOrder, self)._run_move_create()
if self.sale_line_id:
vals.update({'sequence': self.sale_line_id.sequence})
return vals
+15
View File
@@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
class ProductProduct(models.Model):
_inherit = 'product.product'
@api.multi
def _need_procurement(self):
for product in self:
if product.type not in ['service', 'digital']:
return True
return super(ProductProduct, self)._need_procurement()
@@ -1,13 +1,15 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp import fields, models
from odoo import fields, models
class company(models.Model):
_inherit = 'res.company'
security_lead = fields.Float('Sales Safety Days', required=True, default = 0.0,
help="Margin of error for dates promised to customers. "\
"Products will be scheduled for procurement and delivery "\
"that many days earlier than the actual promised date, to "\
security_lead = fields.Float(
'Sales Safety Days', default=0.0, required=True,
help="Margin of error for dates promised to customers. "
"Products will be scheduled for procurement and delivery "
"that many days earlier than the actual promised date, to "
"cope with unexpected delays in the supply chain.")
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp import SUPERUSER_ID
from openerp import api, fields, models, _
from openerp.exceptions import AccessError
from odoo import api, fields, models, _
from odoo.exceptions import AccessError
class SaleConfiguration(models.TransientModel):
_inherit = 'sale.config.settings'
@@ -43,5 +43,4 @@ class SaleConfiguration(models.TransientModel):
default_picking_policy = 'one' if self.default_picking_policy else 'direct'
self.env['ir.values'].sudo().set_default('sale.order', 'picking_policy', default_picking_policy)
res = super(SaleConfiguration, self).set_sale_defaults()
return res
return super(SaleConfiguration, self).set_sale_defaults()
@@ -2,9 +2,10 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, timedelta
from openerp import api, fields, models, _
from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT, float_compare
from openerp.exceptions import UserError
from odoo import api, fields, models, _
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT, float_compare
from odoo.exceptions import UserError
class SaleOrder(models.Model):
@@ -16,13 +17,16 @@ class SaleOrder(models.Model):
warehouse_ids = self.env['stock.warehouse'].search([('company_id', '=', company)], limit=1)
return warehouse_ids
incoterm = fields.Many2one('stock.incoterms', 'Incoterms', help="International Commercial Terms are a series of predefined commercial terms used in international transactions.")
incoterm = fields.Many2one(
'stock.incoterms', 'Incoterms',
help="International Commercial Terms are a series of predefined commercial terms used in international transactions.")
picking_policy = fields.Selection([
('direct', 'Deliver each product when available'),
('one', 'Deliver all products at once')],
string='Shipping Policy', required=True, readonly=True, default='direct',
states={'draft': [('readonly', False)], 'sent': [('readonly', False)]})
warehouse_id = fields.Many2one('stock.warehouse', string='Warehouse',
warehouse_id = fields.Many2one(
'stock.warehouse', string='Warehouse',
required=True, readonly=True, states={'draft': [('readonly', False)], 'sent': [('readonly', False)]},
default=_default_warehouse_id)
picking_ids = fields.Many2many('stock.picking', compute='_compute_picking_ids', string='Picking associated to this sale')
@@ -47,34 +51,20 @@ class SaleOrder(models.Model):
of given sales order ids. It can either be a in a list or in a form
view, if there is only one delivery order to show.
'''
action = self.env.ref('stock.action_picking_tree_all')
action = self.env.ref('stock.action_picking_tree_all').read()[0]
result = {
'name': action.name,
'help': action.help,
'type': action.type,
'view_type': action.view_type,
'view_mode': action.view_mode,
'target': action.target,
'context': action.context,
'res_model': action.res_model,
}
pick_ids = sum([order.picking_ids.ids for order in self], [])
if len(pick_ids) > 1:
result['domain'] = "[('id','in',["+','.join(map(str, pick_ids))+"])]"
elif len(pick_ids) == 1:
form = self.env.ref('stock.view_picking_form', False)
form_id = form.id if form else False
result['views'] = [(form_id, 'form')]
result['res_id'] = pick_ids[0]
return result
pickings = self.mapped('picking_ids')
if len(pickings) > 1:
action['domain'] = [('id', 'in', pickings.ids)]
elif pickings:
action['views'] = [(self.env.ref('stock.view_picking_form').id, 'form')]
action['res_id'] = pickings.id
return action
@api.multi
def action_cancel(self):
self.order_line.mapped('procurement_ids').cancel()
super(SaleOrder, self).action_cancel()
return super(SaleOrder, self).action_cancel()
@api.multi
def _prepare_invoice(self):
@@ -93,6 +83,7 @@ class SaleOrder(models.Model):
super(SaleOrder, self)._get_customer_lead(product_tmpl_id)
return product_tmpl_id.sale_delay
class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'
@@ -127,13 +118,11 @@ class SaleOrderLine(models.Model):
@api.onchange('product_id')
def _onchange_product_id_set_customer_lead(self):
self.customer_lead = self.product_id.sale_delay
return {}
@api.onchange('product_packaging')
def _onchange_product_packaging(self):
if self.product_packaging:
return self._check_package()
return {}
@api.onchange('product_id')
def _onchange_product_id_uom_check_availability(self):
@@ -246,169 +235,3 @@ class SaleOrderLine(models.Model):
break
return is_available
class StockLocationRoute(models.Model):
_inherit = "stock.location.route"
sale_selectable = fields.Boolean(string="Selectable on Sales Order Line")
class AccountInvoice(models.Model):
_inherit = 'account.invoice'
incoterms_id = fields.Many2one('stock.incoterms', string="Incoterms",
help="Incoterms are series of sales terms. They are used to divide transaction costs and responsibilities between buyer and seller and reflect state-of-the-art transportation practices.",
readonly=True, states={'draft': [('readonly', False)]})
class ProcurementOrder(models.Model):
_inherit = "procurement.order"
def _run_move_create(self):
vals = super(ProcurementOrder, self)._run_move_create()
if self.sale_line_id:
vals.update({'sequence': self.sale_line_id.sequence})
return vals
class StockMove(models.Model):
_inherit = "stock.move"
to_refund_so = fields.Boolean(string="To Refund in SO", default=False,
help='Trigger a decrease of the delivered quantity in the associated Sale Order')
@api.multi
def action_done(self):
result = super(StockMove, self).action_done()
# Update delivered quantities on sale order lines
todo = self.env['sale.order.line']
for move in self:
if (move.procurement_id.sale_line_id) and (move.product_id.expense_policy=='no'):
todo |= move.procurement_id.sale_line_id
for line in todo:
line.qty_delivered = line._get_delivered_qty()
return result
@api.multi
def assign_picking(self):
result = super(StockMove, self).assign_picking()
for move in self:
if move.picking_id and move.picking_id.group_id:
picking = move.picking_id
order = self.env['sale.order'].search([('procurement_group_id', '=', picking.group_id.id)])
picking.message_post_with_view('mail.message_origin_link',
values={'self': picking, 'origin': order},
subtype_id=self.env.ref('mail.mt_note').id)
return result
class StockPicking(models.Model):
_inherit = 'stock.picking'
@api.depends('move_lines')
def _compute_sale_id(self):
for picking in self:
sale_order = False
for move in picking.move_lines:
if move.procurement_id.sale_line_id:
sale_order = move.procurement_id.sale_line_id.order_id
break
picking.sale_id = sale_order.id if sale_order else False
def _search_sale_id(self, operator, value):
moves = self.env['stock.move'].search(
[('picking_id', '!=', False), ('procurement_id.sale_line_id.order_id', operator, value)]
)
return [('id', 'in', moves.mapped('picking_id').ids)]
sale_id = fields.Many2one(comodel_name='sale.order', string="Sale Order",
compute='_compute_sale_id', search='_search_sale_id')
@api.multi
def _create_backorder(self, backorder_moves=[]):
res = super(StockPicking, self)._create_backorder(backorder_moves)
for picking in self.filtered(lambda pick: pick.picking_type_id.code == 'outgoing'):
backorder = picking.search([('backorder_id', '=', picking.id)])
order = self.env['sale.order'].search([('procurement_group_id', '=', backorder.group_id.id)])
backorder.message_post_with_view('mail.message_origin_link',
values={'self': backorder, 'origin': order},
subtype_id=self.env.ref('mail.mt_note').id)
return res
class StockReturnPicking(models.TransientModel):
_inherit = "stock.return.picking"
@api.multi
def _create_returns(self):
new_picking_id, pick_type_id = super(StockReturnPicking, self)._create_returns()
new_picking = self.env['stock.picking'].browse([new_picking_id])
for move in new_picking.move_lines:
return_picking_line = self.product_return_moves.filtered(lambda r: r.move_id == move.origin_returned_move_id)
if return_picking_line and return_picking_line.to_refund_so:
move.to_refund_so = True
return new_picking_id, pick_type_id
class StockReturnPickingLine(models.TransientModel):
_inherit = "stock.return.picking.line"
to_refund_so = fields.Boolean(string="To Refund", help='Trigger a decrease of the delivered quantity in the associated Sale Order')
class AccountInvoiceLine(models.Model):
_inherit = "account.invoice.line"
def _get_anglo_saxon_price_unit(self):
price_unit = super(AccountInvoiceLine,self)._get_anglo_saxon_price_unit()
# in case of anglo saxon with a product configured as invoiced based on delivery, with perpetual
# valuation and real price costing method, we must find the real price for the cost of good sold
uom_obj = self.env['product.uom']
if self.product_id.invoice_policy == "delivery":
for s_line in self.sale_line_ids:
# qtys already invoiced
qty_done = sum([uom_obj._compute_qty_obj(x.uom_id, x.quantity, x.product_id.uom_id) for x in s_line.invoice_lines if x.invoice_id.state in ('open', 'paid')])
quantity = uom_obj._compute_qty_obj(self.uom_id, self.quantity, self.product_id.uom_id)
# Put moves in fixed order by date executed
moves = self.env['stock.move']
for procurement in s_line.procurement_ids:
moves |= procurement.move_ids
moves.sorted(lambda x: x.date)
# Go through all the moves and do nothing until you get to qty_done
# Beyond qty_done we need to calculate the average of the price_unit
# on the moves we encounter.
average_price_unit = self._compute_average_price(qty_done, quantity, moves)
price_unit = average_price_unit or price_unit
price_unit = uom_obj._compute_qty_obj(self.uom_id, price_unit, self.product_id.uom_id, round=False)
return price_unit
def _compute_average_price(self, qty_done, quantity, moves):
average_price_unit = 0
qty_delivered = 0
invoiced_qty = 0
for move in moves:
if move.state != 'done':
continue
invoiced_qty += move.product_qty
if invoiced_qty <= qty_done:
continue
qty_to_consider = move.product_qty
if invoiced_qty - move.product_qty < qty_done:
qty_to_consider = invoiced_qty - qty_done
qty_to_consider = min(qty_to_consider, quantity - qty_delivered)
qty_delivered += qty_to_consider
average_price_unit = (average_price_unit * (qty_delivered - qty_to_consider) + move.price_unit * qty_to_consider) / qty_delivered
if qty_delivered == quantity:
break
return average_price_unit
class ProductProduct(models.Model):
_inherit = 'product.product'
@api.multi
def _need_procurement(self):
for product in self:
if product.type not in ['service', 'digital']:
return True
return super(ProductProduct, self)._need_procurement()
+94
View File
@@ -0,0 +1,94 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class StockLocationRoute(models.Model):
_inherit = "stock.location.route"
sale_selectable = fields.Boolean("Selectable on Sales Order Line")
class StockMove(models.Model):
_inherit = "stock.move"
to_refund_so = fields.Boolean(
"To Refund in SO", default=False,
help='Trigger a decrease of the delivered quantity in the associated Sale Order')
@api.multi
def action_done(self):
result = super(StockMove, self).action_done()
# Update delivered quantities on sale order lines
sale_order_lines = self.filtered(lambda move: move.procurement_id.sale_line_id and move.product_id.expense_policy == 'no').mapped('procurement_id.sale_line_id')
for line in sale_order_lines:
line.qty_delivered = line._get_delivered_qty()
return result
@api.multi
def assign_picking(self):
result = super(StockMove, self).assign_picking()
for move in self:
if move.picking_id and move.picking_id.group_id:
picking = move.picking_id
order = self.env['sale.order'].search([('procurement_group_id', '=', picking.group_id.id)])
picking.message_post_with_view(
'mail.message_origin_link',
values={'self': picking, 'origin': order},
subtype_id=self.env.ref('mail.mt_note').id)
return result
class StockPicking(models.Model):
_inherit = 'stock.picking'
sale_id = fields.Many2one('sale.order', "Sale Order", compute='_compute_sale_id', search='_search_sale_id')
@api.one
@api.depends('move_lines.procurement_id.sale_line_id.order_id')
def _compute_sale_id(self):
for move in self.move_lines:
if move.procurement_id.sale_line_id:
self.sale_id = move.procurement_id.sale_line_id.order_id
return
def _search_sale_id(self, operator, value):
moves = self.env['stock.move'].search(
[('picking_id', '!=', False), ('procurement_id.sale_line_id.order_id', operator, value)]
)
return [('id', 'in', moves.mapped('picking_id').ids)]
@api.multi
def _create_backorder(self, backorder_moves=[]):
res = super(StockPicking, self)._create_backorder(backorder_moves)
for picking in self.filtered(lambda pick: pick.picking_type_id.code == 'outgoing'):
backorder = picking.search([('backorder_id', '=', picking.id)])
order = self.env['sale.order'].search([('procurement_group_id', '=', backorder.group_id.id)])
backorder.message_post_with_view(
'mail.message_origin_link',
values={'self': backorder, 'origin': order},
subtype_id=self.env.ref('mail.mt_note').id)
return res
class StockReturnPicking(models.TransientModel):
_inherit = "stock.return.picking"
@api.multi
def _create_returns(self):
new_picking_id, pick_type_id = super(StockReturnPicking, self)._create_returns()
new_picking = self.env['stock.picking'].browse([new_picking_id])
for move in new_picking.move_lines:
return_picking_line = self.product_return_moves.filtered(lambda r: r.move_id == move.origin_returned_move_id)
if return_picking_line and return_picking_line.to_refund_so:
move.to_refund_so = True
return new_picking_id, pick_type_id
class StockReturnPickingLine(models.TransientModel):
_inherit = "stock.return.picking.line"
to_refund_so = fields.Boolean(string="To Refund", help='Trigger a decrease of the delivered quantity in the associated Sale Order')
@@ -0,0 +1,11 @@
<?xml version="1.0"?>
<openerp>
<template id="report_saleorder_document_inherit_sale_stock" inherit_id="sale.report_saleorder_document">
<xpath expr="//div[@name='payment_term']" position="after">
<div class="col-xs-3" t-if="doc.incoterm" groups="sale.group_display_incoterm">
<strong>Incoterms:</strong>
<p t-field="doc.incoterm.code"/>
</div>
</xpath>
</template>
</openerp>
+1 -1
View File
@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp import fields, models
-12
View File
@@ -1,12 +0,0 @@
-
Actions that need an income account are done here
-
!python {model: ir.model.data} : |
account_id = self.pool.get('account.account').search(cr, uid, [('user_type_id', '=', ref('account.data_account_type_revenue'))])
if account_id:
vals = {
'warehouse_id': ref('stock.warehouse0'),
}
self._update(cr, uid, 'sale.order', 'sale', vals, 'sale_order_4')
self.pool.get('sale.order').signal_workflow(cr, uid, [ref('sale.sale_order_1')], 'order_confirm')
-74
View File
@@ -1,74 +0,0 @@
<?xml version="1.0"?>
<openerp>
<data>
<record id="view_order_form_inherit_sale_stock" model="ir.ui.view">
<field name="name">sale.order.form.sale.stock</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<data>
<xpath expr="//button[@name='action_view_invoice']" position="before">
<field name="picking_ids" invisible="1"/>
<button type="object"
name="action_view_delivery"
class="oe_stat_button"
icon="fa-truck"
attrs="{'invisible': [('delivery_count', '=', 0)]}" groups="base.group_user">
<field name="delivery_count" widget="statinfo" string="Delivery"/>
</button>
</xpath>
<xpath expr="//group[@name='sales_person']" position="before">
<group string="Shipping Information" name="sale_shipping">
<field name="warehouse_id" options="{'no_create': True}" groups="stock.group_stock_multi_locations"/>
<field name="incoterm" widget="selection" groups="base.group_user"/>
<field name="picking_policy" required="True"/>
</group>
</xpath>
<xpath expr="//page/field[@name='order_line']/form/group/group/field[@name='tax_id']" position="before">
<field name="product_tmpl_id" invisible="1"/>
<field name="product_packaging" context="{'default_product_tmpl_id': product_tmpl_id, 'partner_id':parent.partner_id, 'quantity':product_uom_qty, 'pricelist':parent.pricelist_id, 'uom':product_uom, 'company_id': parent.company_id}" domain="[('product_tmpl_id','=',product_tmpl_id)]" groups="product.group_stock_packaging" />
</xpath>
<xpath expr="//field[@name='order_line']/form/group/group/field[@name='price_unit']" position="before">
<field name="route_id" groups="sale_stock.group_route_so_lines"/>
</xpath>
<xpath expr="//field[@name='order_line']/tree/field[@name='price_unit']" position="before">
<field name="route_id" groups="sale_stock.group_route_so_lines"/>
</xpath>
</data>
</field>
</record>
<record id="view_order_line_tree_inherit_sale_stock" model="ir.ui.view">
<field name="name">sale.order.line.tree.sale.stock.location</field>
<field name="inherit_id" ref="sale.view_order_line_tree"/>
<field name="model">sale.order.line</field>
<field name="arch" type="xml">
<xpath expr="//field[@name='price_subtotal']" position="before">
<field name="route_id" groups="sale_stock.group_route_so_lines"/>
</xpath>
</field>
</record>
<record id="view_stock_return_picking_form_inherit_sale_stock" model="ir.ui.view">
<field name="name">stock.return.picking.sale.stock.form</field>
<field name="inherit_id" ref="stock.view_stock_return_picking_form"/>
<field name="model">stock.return.picking</field>
<field name="arch" type="xml">
<field name="quantity" position="after">
<field name="to_refund_so"/>
</field>
</field>
</record>
<template id="report_saleorder_document_inherit_sale_stock" inherit_id="sale.report_saleorder_document">
<xpath expr="//div[@name='payment_term']" position="after">
<div class="col-xs-3" t-if="doc.incoterm" groups="sale.group_display_incoterm">
<strong>Incoterms:</strong>
<p t-field="doc.incoterm.code"/>
</div>
</xpath>
</template>
</data>
</openerp>
@@ -1,5 +1,5 @@
<?xml version="1.0" ?>
<openerp>
<odoo>
<data>
<record id="view_company_form_inherit_sale_stock" model="ir.ui.view">
<field name="name">res.company.mrp.config</field>
@@ -13,4 +13,4 @@
</field>
</record>
</data>
</openerp>
</odoo>
@@ -0,0 +1,50 @@
<?xml version="1.0"?>
<odoo>
<data>
<record id="view_order_form_inherit_sale_stock" model="ir.ui.view">
<field name="name">sale.order.form.sale.stock</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<xpath expr="//button[@name='action_view_invoice']" position="before">
<field name="picking_ids" invisible="1"/>
<button type="object"
name="action_view_delivery"
class="oe_stat_button"
icon="fa-truck"
attrs="{'invisible': [('delivery_count', '=', 0)]}" groups="base.group_user">
<field name="delivery_count" widget="statinfo" string="Delivery"/>
</button>
</xpath>
<xpath expr="//group[@name='sales_person']" position="before">
<group string="Shipping Information" name="sale_shipping">
<field name="warehouse_id" options="{'no_create': True}" groups="stock.group_stock_multi_locations"/>
<field name="incoterm" widget="selection" groups="base.group_user"/>
<field name="picking_policy" required="True"/>
</group>
</xpath>
<xpath expr="//page/field[@name='order_line']/form/group/group/field[@name='tax_id']" position="before">
<field name="product_tmpl_id" invisible="1"/>
<field name="product_packaging" context="{'default_product_tmpl_id': product_tmpl_id, 'partner_id':parent.partner_id, 'quantity':product_uom_qty, 'pricelist':parent.pricelist_id, 'uom':product_uom, 'company_id': parent.company_id}" domain="[('product_tmpl_id','=',product_tmpl_id)]" groups="product.group_stock_packaging" />
</xpath>
<xpath expr="//field[@name='order_line']/form/group/group/field[@name='price_unit']" position="before">
<field name="route_id" groups="sale_stock.group_route_so_lines"/>
</xpath>
<xpath expr="//field[@name='order_line']/tree/field[@name='price_unit']" position="before">
<field name="route_id" groups="sale_stock.group_route_so_lines"/>
</xpath>
</field>
</record>
<record id="view_order_line_tree_inherit_sale_stock" model="ir.ui.view">
<field name="name">sale.order.line.tree.sale.stock.location</field>
<field name="inherit_id" ref="sale.view_order_line_tree"/>
<field name="model">sale.order.line</field>
<field name="arch" type="xml">
<xpath expr="//field[@name='price_subtotal']" position="before">
<field name="route_id" groups="sale_stock.group_route_so_lines"/>
</xpath>
</field>
</record>
</data>
</odoo>
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<odoo>
<data>
<menuitem id="base.menu_aftersale" name="After-Sale"
@@ -18,5 +18,16 @@
</field>
</record>
<record id="view_stock_return_picking_form_inherit_sale_stock" model="ir.ui.view">
<field name="name">stock.return.picking.sale.stock.form</field>
<field name="inherit_id" ref="stock.view_stock_return_picking_form"/>
<field name="model">stock.return.picking</field>
<field name="arch" type="xml">
<field name="quantity" position="after">
<field name="to_refund_so"/>
</field>
</field>
</record>
</data>
</openerp>
</odoo>
+2 -3
View File
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import product
import stock_account
import models
import report
import wizard
import res_config
+10 -10
View File
@@ -24,22 +24,22 @@ Dashboard / Reports for Warehouse Management includes:
'category': 'Hidden',
'sequence': 16,
'demo': [
'stock_account_demo.yml'
'data/stock_account_demo.yml'
],
'data': [
'security/stock_account_security.xml',
'security/ir.model.access.csv',
'stock_account_data.xml',
'wizard/stock_change_standard_price_view.xml',
'wizard/stock_valuation_history_view.xml',
'product_data.xml',
'product_view.xml',
'stock_account_view.xml',
'res_config_view.xml',
'stock_account_post_install.yml',
'wizard/stock_change_standard_price_views.xml',
'wizard/wizard_valuation_history_views.xml',
'views/product_views.xml',
'views/stock_account_views.xml',
'views/stock_config_settings_views.xml',
'report/stock_history_views.xml',
'data/product_data.xml',
'data/stock_account_data.xml',
'data/stock_account_data_post_install.yml',
],
'test': [
],
'installable': True,
'auto_install': True,
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<odoo>
<data noupdate="1">
<record forcecreate="True" id="property_stock_account_output_prd" model="ir.property">
<field name="name">property_stock_account_output</field>
<field name="fields_id" search="[('model','=','product.template'),('name','=','property_stock_account_output')]"/>
@@ -26,6 +25,5 @@
<field eval="False" name="value"/>
<field name="company_id" ref="base.main_company"/>
</record>
</data>
</openerp>
</odoo>
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<odoo>
<data noupdate="1">
<record forcecreate="True" id="default_cost_method" model="ir.property">
<field name="name">Cost Method Property</field>
<field name="fields_id" ref="field_product_template_property_cost_method"/>
@@ -27,6 +26,5 @@
<field name="value">manual_periodic</field>
<field name="type">selection</field>
</record>
</data>
</openerp>
</odoo>
+8
View File
@@ -0,0 +1,8 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import account_chart_template
import account_invoice
import product
import stock
import stock_config_settings
@@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models, _
import logging
_logger = logging.getLogger(__name__)
class AccountChartTemplate(models.Model):
_inherit = "account.chart.template"
@api.model
def generate_journals(self, acc_template_ref, company, journals_dict=None):
journal_to_add = [{'name': _('Stock Journal'), 'type': 'general', 'code': 'STJ', 'favorite': False, 'sequence': 8}]
return super(AccountChartTemplate, self).generate_journals(acc_template_ref=acc_template_ref, company=company, journals_dict=journal_to_add)
@api.multi
def generate_properties(self, acc_template_ref, company, property_list=None):
res = super(AccountChartTemplate, self).generate_properties(acc_template_ref=acc_template_ref, company=company)
PropertyObj = self.env['ir.property'] # Property Stock Journal
value = self.env['account.journal'].search([('company_id', '=', company.id), ('code', '=', 'STJ'), ('type', '=', 'general')], limit=1)
if value:
field = self.env['ir.model.fields'].search([('name', '=', 'property_stock_journal'), ('model', '=', 'product.category'), ('relation', '=', 'account.journal')], limit=1)
vals = {
'name': 'property_stock_journal',
'company_id': company.id,
'fields_id': field.id,
'value': 'account.journal,%s' % value.id,
}
properties = PropertyObj.search([('name', '=', 'property_stock_journal'), ('company_id', '=', company.id)])
if properties:
# the property exist: modify it
properties.write(vals)
else:
# create the property
PropertyObj.create(vals)
todo_list = [ # Property Stock Accounts
'property_stock_account_input_categ_id',
'property_stock_account_output_categ_id',
'property_stock_valuation_account_id',
]
for record in todo_list:
account = getattr(self, record)
value = account and 'account.account,' + str(acc_template_ref[account.id]) or False
if value:
field = self.env['ir.model.fields'].search([('name', '=', record), ('model', '=', 'product.category'), ('relation', '=', 'account.account')], limit=1)
vals = {
'name': record,
'company_id': company.id,
'fields_id': field.id,
'value': value,
}
properties = PropertyObj.search([('name', '=', record), ('company_id', '=', company.id)])
if properties:
# the property exist: modify it
properties.write(vals)
else:
# create the property
PropertyObj.create(vals)
return res
@@ -0,0 +1,87 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models
import logging
_logger = logging.getLogger(__name__)
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.company_id.anglo_saxon_accounting and self.type in ('out_invoice', 'out_refund'):
for i_line in self.invoice_line_ids:
res.extend(self._anglo_saxon_sale_move_lines(i_line))
return res
@api.model
def _anglo_saxon_sale_move_lines(self, i_line):
"""Return the additional move lines for sales 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.type == 'product' and i_line.product_id.valuation == 'real_time':
fpos = i_line.invoice_id.fiscal_position_id
accounts = i_line.product_id.product_tmpl_id.get_product_accounts(fiscal_pos=fpos)
# debit account dacc will be the output account
dacc = accounts['stock_output'].id
# credit account cacc will be the expense account
cacc = accounts['expense'].id
if dacc and cacc:
price_unit = i_line._get_anglo_saxon_price_unit()
return [
{
'type': 'src',
'name': i_line.name[:64],
'price_unit': price_unit,
'quantity': i_line.quantity,
'price': i_line._get_price(company_currency, price_unit),
'account_id':dacc,
'product_id':i_line.product_id.id,
'uom_id':i_line.uom_id.id,
'account_analytic_id': False,
},
{
'type': 'src',
'name': i_line.name[:64],
'price_unit': price_unit,
'quantity': i_line.quantity,
'price': -1 * i_line._get_price(company_currency, price_unit),
'account_id':cacc,
'product_id':i_line.product_id.id,
'uom_id':i_line.uom_id.id,
'account_analytic_id': False,
},
]
return []
class AccountInvoiceLine(models.Model):
_inherit = "account.invoice.line"
def _get_anglo_saxon_price_unit(self):
self.ensure_one()
return self.product_id.standard_price
def _get_price(self, company_currency, price_unit):
if self.invoice_id.currency_id.id != company_currency:
price = company_currency.with_context(date=self.invoice_id.date_invoice).compute(price_unit * self.quantity, self.invoice_id.currency_id.id)
else:
price = price_unit * self.quantity
return round(price, self.invoice_id.currency_id.decimal_places)
def get_invoice_line_account(self, type, product, fpos, company):
if company.anglo_saxon_accounting and type in ('in_invoice', 'in_refund') and product and product.type == 'product':
accounts = product.product_tmpl_id.get_product_accounts(fiscal_pos=fpos)
return accounts['stock_input']
return super(AccountInvoiceLine, self).get_invoice_line_account(type, product, fpos, company)
+187
View File
@@ -0,0 +1,187 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, tools, _
from odoo.addons import decimal_precision as dp
from odoo.exceptions import UserError
class ProductTemplate(models.Model):
_name = 'product.template'
_inherit = 'product.template'
property_valuation = fields.Selection([
('manual_periodic', 'Periodic (manual)'),
('real_time', 'Perpetual (automated)')], string='Inventory Valuation',
company_dependent=True, copy=True, default='manual_periodic',
help="If perpetual valuation is enabled for a product, the system will automatically create journal entries corresponding to stock moves, with product price as specified by the 'Costing Method'" \
"The inventory variation account set on the product category will represent the current inventory value, and the stock input and stock output account will hold the counterpart moves for incoming and outgoing products.")
valuation = fields.Char(compute='_compute_valuation_type', inverse='_set_valuation_type')
property_cost_method = fields.Selection([
('standard', 'Standard Price'),
('average', 'Average Price'),
('real', 'Real Price')], string='Costing Method',
company_dependent=True, copy=True,
help="""Standard Price: The cost price is manually updated at the end of a specific period (usually once a year).
Average Price: The cost price is recomputed at each incoming shipment and used for the product valuation.
Real Price: The cost price displayed is the price of the last outgoing product (will be use in case of inventory loss for example).""")
cost_method = fields.Char(compute='_compute_cost_method', inverse='_set_cost_method')
property_stock_account_input = fields.Many2one(
'account.account', 'Stock Input Account',
company_dependent=True, domain=[('deprecated', '=', False)],
help="When doing real-time inventory valuation, counterpart journal items for all incoming stock moves will be posted in this account, unless "
"there is a specific valuation account set on the source location. When not set on the product, the one from the product category is used.")
property_stock_account_output = fields.Many2one(
'account.account', 'Stock Output Account',
company_dependent=True, domain=[('deprecated', '=', False)],
help="When doing real-time inventory valuation, counterpart journal items for all outgoing stock moves will be posted in this account, unless "
"there is a specific valuation account set on the destination location. When not set on the product, the one from the product category is used.")
@api.one
@api.depends('property_valuation', 'categ_id.property_valuation')
def _compute_valuation_type(self):
self.valuation = self.property_valuation if self.property_valuation else self.categ_id.property_valuation
@api.one
def _set_valuation_type(self):
return self.write({'property_valuation': self.valuation})
@api.one
@api.depends('property_cost_method', 'categ_id.property_cost_method')
def _compute_cost_method(self):
self.cost_method = self.property_cost_method if self.property_cost_method else self.categ_id.property_cost_method
@api.one
def _set_cost_method(self):
return self.write({'property_cost_method': self.cost_method})
@api.onchange('type')
def onchange_type_valuation(self):
if self.type != 'product':
self.valuation = 'manual_periodic'
return {}
@api.multi
def _get_product_accounts(self):
""" Add the stock accounts related to product to the result of super()
@return: dictionary which contains information regarding stock accounts and super (income+expense accounts)
"""
accounts = super(ProductTemplate, self)._get_product_accounts()
res = self._get_asset_accounts()
accounts.update({
'stock_input': res['stock_input'] or self.property_stock_account_input or self.categ_id.property_stock_account_input_categ_id,
'stock_output': res['stock_output'] or self.property_stock_account_output or self.categ_id.property_stock_account_output_categ_id,
'stock_valuation': self.categ_id.property_stock_valuation_account_id or False,
})
return accounts
@api.multi
def get_product_accounts(self, fiscal_pos=None):
""" Add the stock journal related to product to the result of super()
@return: dictionary which contains all needed information regarding stock accounts and journal and super (income+expense accounts)
"""
accounts = super(ProductTemplate, self).get_product_accounts(fiscal_pos=fiscal_pos)
accounts.update({'stock_journal': self.categ_id.property_stock_journal or False})
return accounts
class ProductProduct(models.Model):
_inherit = 'product.product'
@api.onchange('type')
def onchange_type_valuation(self):
if self.type != 'product':
self.valuation = 'manual_periodic'
return {}
@api.multi
def do_change_standard_price(self, new_price, account_id):
""" Changes the Standard Price of Product and creates an account move accordingly."""
AccountMove = self.env['account.move']
locations = self.env['stock.location'].search([('usage', '=', 'internal'), ('company_id', '=', self.env.user.company_id.id)])
product_accounts = {(product.id, product.product_tmpl_id.get_product_accounts()) for product in self}
price_precision = dp.get_precision('Product Price')
for location in locations:
for product in self.with_context(location=location.id, compute_child=False):
diff = product.standard_price - new_price
if tools.float_is_zero(diff, precision_digits=price_precision):
raise UserError(_("No difference between standard price and new price!"))
qty_available = product.qty_available
if qty_available:
# Accounting Entries
if diff * qty_available > 0:
debit_account_id = account_id
credit_account_id = product_accounts[product.id]['stock_valuation'].id
else:
debit_account_id = product_accounts[product.id]['stock_valuation'].id
credit_account_id = account_id
move_vals = {
'journal_id': product_accounts[product.id]['stock_journal'].id,
'company_id': location.company_id.id,
'line_ids': [(0, 0, {
'name': _('Standard Price changed'),
'account_id': debit_account_id,
'debit': abs(diff * qty_available),
'credit': 0,
}), (0, 0, {
'name': _('Standard Price changed'),
'account_id': credit_account_id,
'debit': 0,
'credit': abs(diff * qty_available),
})],
}
move = AccountMove.create(move_vals)
move.post()
self.write({'standard_price': new_price})
return True
class ProductCategory(models.Model):
_inherit = 'product.category'
property_valuation = fields.Selection([
('manual_periodic', 'Periodic (manual)'),
('real_time', 'Perpetual (automated)')], string='Inventory Valuation',
company_dependent=True, copy=True, required=True,
help="If perpetual valuation is enabled for a product, the system "
"will automatically create journal entries corresponding to "
"stock moves, with product price as specified by the 'Costing "
"Method'. The inventory variation account set on the product "
"category will represent the current inventory value, and the "
"stock input and stock output account will hold the counterpart "
"moves for incoming and outgoing products.")
property_cost_method = fields.Selection([
('standard', 'Standard Price'),
('average', 'Average Price'),
('real', 'Real Price')], string="Costing Method",
company_dependent=True, copy=True, required=True,
help="Standard Price: The cost price is manually updated at the end "
"of a specific period (usually once a year).\nAverage Price: "
"The cost price is recomputed at each incoming shipment and "
"used for the product valuation.\nReal Price: The cost price "
"displayed is the price of the last outgoing product (will be "
"used in case of inventory loss for example).""")
property_stock_journal = fields.Many2one(
'account.journal', 'Stock Journal', company_dependent=True,
help="When doing real-time inventory valuation, this is the Accounting Journal in which entries will be automatically posted when stock moves are processed.")
property_stock_account_input_categ_id = fields.Many2one(
'account.account', 'Stock Input Account', company_dependent=True,
domain=[('deprecated', '=', False)], oldname="property_stock_account_input_categ",
help="When doing real-time inventory valuation, counterpart journal items for all incoming stock moves will be posted in this account, unless "
"there is a specific valuation account set on the source location. This is the default value for all products in this category. It "
"can also directly be set on each product")
property_stock_account_output_categ_id = fields.Many2one(
'account.account', 'Stock Output Account', company_dependent=True,
domain=[('deprecated', '=', False)], oldname="property_stock_account_output_categ",
help="When doing real-time inventory valuation, counterpart journal items for all outgoing stock moves will be posted in this account, unless "
"there is a specific valuation account set on the destination location. This is the default value for all products in this category. It "
"can also directly be set on each product")
property_stock_valuation_account_id = fields.Many2one(
'account.account', 'Stock Valuation Account', company_dependent=True,
domain=[('deprecated', '=', False)],
help="When real-time inventory valuation is enabled on a product, this account will hold the current value of the products.",)
+329
View File
@@ -0,0 +1,329 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import defaultdict
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.tools import float_compare, float_round
import logging
_logger = logging.getLogger(__name__)
class StockInventory(models.Model):
_inherit = "stock.inventory"
accounting_date = fields.Date(
'Force Accounting Date',
help="Choose the accounting date at which you want to value the stock "
"moves created by the inventory instead of the default one (the "
"inventory end date)")
@api.multi
def post_inventory(self):
acc_inventories = self.filtered(lambda inventory: inventory.accounting_date)
for inventory in acc_inventories:
res = super(StockInventory, inventory.with_context(force_period_date=inventory.accounting_date)).post_inventory()
other_inventories = self - acc_inventories
if other_inventories:
res = super(StockInventory, other_inventories).post_inventory()
return res
class StockLocation(models.Model):
_inherit = "stock.location"
valuation_in_account_id = fields.Many2one(
'account.account', 'Stock Valuation Account (Incoming)',
domain=[('internal_type', '=', 'other'), ('deprecated', '=', False)],
help="Used for real-time inventory valuation. When set on a virtual location (non internal type), "
"this account will be used to hold the value of products being moved from an internal location "
"into this location, instead of the generic Stock Output Account set on the product. "
"This has no effect for internal locations.")
valuation_out_account_id = fields.Many2one(
'account.account', 'Stock Valuation Account (Outgoing)',
domain=[('internal_type', '=', 'other'), ('deprecated', '=', False)],
help="Used for real-time inventory valuation. When set on a virtual location (non internal type), "
"this account will be used to hold the value of products being moved out of this location "
"and into an internal location, instead of the generic Stock Output Account set on the product. "
"This has no effect for internal locations.")
class StockQuant(models.Model):
_inherit = "stock.quant"
@api.multi
def _compute_inventory_value(self):
real_value_quants = self.filtered(lambda quant: quant.product_id.cost_method == 'real')
for quant in real_value_quants:
quant.inventory_value = quant.cost * quant.qty
return super(StockQuant, self - real_value_quants)._compute_inventory_value()
@api.multi
def _price_update(self, newprice):
''' This function is called at the end of negative quant reconciliation
and does the accounting entries adjustemnts and the update of the product
cost price if needed '''
super(StockQuant, self)._price_update(newprice)
for quant in self:
move = quant._get_latest_move()
valuation_update = newprice - quant.cost
# this is where we post accounting entries for adjustment, if needed
# If neg quant period already closed (likely with manual valuation), skip update
if not quant.company_id.currency_id.is_zero(valuation_update) and move._check_lock_date():
quant.with_context(force_valuation_amount=valuation_update)._account_entry_move(move)
# update the standard price of the product, only if we would have
# done it if we'd have had enough stock at first, which means
# 1) the product cost's method is 'real'
# 2) we just fixed a negative quant caused by an outgoing shipment
if quant.product_id.cost_method == 'real' and quant.location_id.usage != 'internal':
move._store_average_cost_price()
def _account_entry_move(self, move):
""" Accounting Valuation Entries """
if move.product_id.type != 'product' or move.product_id.valuation != 'real_time':
# no stock valuation for consumable products
return False
if any(quant.owner_id or quant.qty <= 0 for quant in self):
# if the quant isn't owned by the company, we don't make any valuation en
# we don't make any stock valuation for negative quants because the valuation is already made for the counterpart.
# At that time the valuation will be made at the product cost price and afterward there will be new accounting entries
# to make the adjustments when we know the real cost price.
return False
location_from = move.location_id
location_to = self[0].location_id # TDE FIXME: as the accounting is based on this value, should probably check all location_to to be the same
company_from = location_from.usage == 'internal' and location_from.company_id or False
company_to = location_to and (location_to.usage == 'internal') and location_to.company_id or False
# Create Journal Entry for products arriving in the company; in case of routes making the link between several
# warehouse of the same company, the transit location belongs to this company, so we don't need to create accounting entries
if company_to and (move.location_id.usage not in ('internal', 'transit') and move.location_dest_id.usage == 'internal' or company_from != company_to):
journal_id, acc_src, acc_dest, acc_valuation = move._get_accounting_data_for_valuation()
if location_from and location_from.usage == 'customer': # goods returned from customer
self.with_context(force_company=company_to.id)._create_account_move_line(move, acc_dest, acc_valuation, journal_id)
else:
self.with_context(force_company=company_to.id)._create_account_move_line(move, acc_src, acc_valuation, journal_id)
# Create Journal Entry for products leaving the company
if company_from and (move.location_id.usage == 'internal' and move.location_dest_id.usage not in ('internal', 'transit') or company_from != company_to):
journal_id, acc_src, acc_dest, acc_valuation = move._get_accounting_data_for_valuation()
if location_to and location_to.usage == 'supplier': # goods returned to supplier
self.with_context(force_company=company_from.id)._create_account_move_line(move, acc_valuation, acc_src, journal_id)
else:
self.with_context(force_company=company_from.id)._create_account_move_line(move, acc_valuation, acc_dest, journal_id)
def _create_account_move_line(self, move, credit_account_id, debit_account_id, journal_id):
# group quants by cost
quant_cost_qty = defaultdict(lambda: 0.0)
for quant in self:
quant_cost_qty[quant.cost] += quant.qty
AccountMove = self.env['account.move']
for cost, qty in quant_cost_qty.iteritems():
move_lines = move._prepare_account_move_line(qty, cost, credit_account_id, debit_account_id)
date = self._context.get('force_period_date', fields.Date.context_today(self))
new_account_move = AccountMove.create({
'journal_id': journal_id,
'line_ids': move_lines,
'date': date,
'ref': move.picking_id.name})
new_account_move.post()
def _quant_create_from_move(self, qty, move, lot_id=False, owner_id=False, src_package_id=False, dest_package_id=False, force_location_from=False, force_location_to=False):
quant = super(StockQuant, self)._quant_create_from_move(qty, move, lot_id=lot_id, owner_id=owner_id, src_package_id=src_package_id, dest_package_id=dest_package_id, force_location_from=force_location_from, force_location_to=force_location_to)
quant._account_entry_move(move)
if move.product_id.valuation == 'real_time':
# If the precision required for the variable quant cost is larger than the accounting
# precision, inconsistencies between the stock valuation and the accounting entries
# may arise.
# For example, a box of 13 units is bought 15.00. If the products leave the
# stock one unit at a time, the amount related to the cost will correspond to
# round(15/13, 2)*13 = 14.95. To avoid this case, we split the quant in 12 + 1, then
# record the difference on the new quant.
# We need to make sure to able to extract at least one unit of the product. There is
# an arbitrary minimum quantity set to 2.0 from which we consider we can extract a
# unit and adapt the cost.
curr_rounding = move.company_id.currency_id.rounding
cost_rounded = float_round(quant.cost, precision_rounding=curr_rounding)
cost_correct = cost_rounded
if float_compare(quant.product_id.uom_id.rounding, 1.0, precision_digits=1) == 0\
and float_compare(quant.qty * quant.cost, quant.qty * cost_rounded, precision_rounding=curr_rounding) != 0\
and float_compare(quant.qty, 2.0, precision_rounding=quant.product_id.uom_id.rounding) >= 0:
quant_correct = quant._quant_split(quant.qty - 1.0)
cost_correct += (quant.qty * quant.cost) - (quant.qty * cost_rounded)
quant.sudo().write({'cost': cost_rounded})
quant_correct.sudo().write({'cost': cost_correct})
return quant
def _quant_update_from_move(self, move, location_dest_id, dest_package_id, lot_id=False, entire_pack=False):
res = super(StockQuant, self)._quant_update_from_move(move, location_dest_id, dest_package_id, lot_id=lot_id, entire_pack=entire_pack)
self._account_entry_move(move)
return res
class StockMove(models.Model):
_inherit = "stock.move"
@api.multi
def action_done(self):
self.product_price_update_before_done()
res = super(StockMove, self).action_done()
self.product_price_update_after_done()
return res
@api.multi
def product_price_update_before_done(self):
tmpl_dict = defaultdict(lambda: 0.0)
# adapt standard price on incomming moves if the product cost_method is 'average'
for move in self.filtered(lambda move: move.location_id.usage == 'supplier' and move.product_id.cost_method == 'average'):
product_tot_qty_available = move.product_id.qty_available + tmpl_dict[move.product_id.id]
# if the incoming move is for a purchase order with foreign currency, need to call this to get the same value that the quant will use.
if product_tot_qty_available <= 0:
new_std_price = move.get_price_unit()
else:
# Get the standard price
amount_unit = move.product_id.standard_price
new_std_price = ((amount_unit * product_tot_qty_available) + (move.get_price_unit() * move.product_qty)) / (product_tot_qty_available + move.product_qty)
tmpl_dict[move.product_id.id] += move.product_qty
# Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products
move.product_id.with_context(force_company=move.company_id.id).write({'standard_price': new_std_price})
@api.multi
def product_price_update_after_done(self):
''' Adapt standard price on outgoing moves if the product cost_method is 'real', so that a
return or an inventory loss is made using the last value used for an outgoing valuation. '''
to_update_moves = self.filtered(lambda move: move.product_id.cost_method == 'real' and move.location_dest_id.usage != 'internal')
to_update_moves._store_average_cost_price()
def _store_average_cost_price(self):
""" Store the average price of the move on the move and product form """
for move in self:
# product_obj = self.pool.get('product.product')
if any(q.qty <= 0 for q in move.quant_ids) or move.product_qty == 0:
# if there is a negative quant, the standard price shouldn't be updated
return
# Note: here we can't store a quant.cost directly as we may have moved out 2 units
# (1 unit to 5€ and 1 unit to 7€) and in case of a product return of 1 unit, we can't
# know which of the 2 costs has to be used (5€ or 7€?). So at that time, thanks to the
# average valuation price we are storing we will valuate it at 6€
valuation_price = sum(q.qty * q.cost for q in move.quant_ids)
average_valuation_price = valuation_price / move.product_qty
move.product_id.with_context(force_company=move.company_id.id).sudo().write({'standard_price': average_valuation_price})
move.write({'price_unit': average_valuation_price})
@api.multi
def _get_accounting_data_for_valuation(self):
""" Return the accounts and journal to use to post Journal Entries for
the real-time valuation of the quant. """
self.ensure_one()
accounts_data = self.product_id.product_tmpl_id.get_product_accounts()
if self.location_id.valuation_out_account_id:
acc_src = self.location_id.valuation_out_account_id.id
else:
acc_src = accounts_data['stock_input'].id
if self.location_dest_id.valuation_in_account_id:
acc_dest = self.location_dest_id.valuation_in_account_id.id
else:
acc_dest = accounts_data['stock_output'].id
acc_valuation = accounts_data.get('stock_valuation', False)
if acc_valuation:
acc_valuation = acc_valuation.id
if not accounts_data.get('stock_journal', False):
raise UserError(_('You don\'t have any stock journal defined on your product category, check if you have installed a chart of accounts'))
if not acc_src:
raise UserError(_('Cannot find a stock input account for the product %s. You must define one on the product category, or on the location, before processing this operation.') % (self.product_id.name))
if not acc_dest:
raise UserError(_('Cannot find a stock output account for the product %s. You must define one on the product category, or on the location, before processing this operation.') % (self.product_id.name))
if not acc_valuation:
raise UserError(_('You don\'t have any stock valuation account defined on your product category. You must define one before processing this operation.'))
journal_id = accounts_data['stock_journal'].id
return journal_id, acc_src, acc_dest, acc_valuation
def _prepare_account_move_line(self, qty, cost, credit_account_id, debit_account_id):
"""
Generate the account.move.line values to post to track the stock valuation difference due to the
processing of the given quant.
"""
self.ensure_one()
if self._context.get('force_valuation_amount'):
valuation_amount = self._context.get('force_valuation_amount')
else:
if self.product_id.cost_method == 'average':
valuation_amount = cost if self.location_id.usage == 'supplier' and self.location_dest_id.usage == 'internal' else self.product_id.standard_price
else:
valuation_amount = cost if self.product_id.cost_method == 'real' else self.product_id.standard_price
# the standard_price of the product may be in another decimal precision, or not compatible with the coinage of
# the company currency... so we need to use round() before creating the accounting entries.
debit_value = self.company_id.currency_id.round(valuation_amount * qty)
# check that all data is correct
if self.company_id.currency_id.is_zero(debit_value):
raise UserError(_("The found valuation amount for product %s is zero. Which means there is probably a configuration error. Check the costing method and the standard price") % (self.product_id.name,))
credit_value = debit_value
if self.product_id.cost_method == 'average' and self.company_id.anglo_saxon_accounting:
# in case of a supplier return in anglo saxon mode, for products in average costing method, the stock_input
# account books the real purchase price, while the stock account books the average price. The difference is
# booked in the dedicated price difference account.
if self.location_dest_id.usage == 'supplier' and self.origin_returned_move_id and self.origin_returned_move_id.purchase_line_id:
debit_value = self.origin_returned_move_id.price_unit * qty
# in case of a customer return in anglo saxon mode, for products in average costing method, the stock valuation
# is made using the original average price to negate the delivery effect.
if self.location_id.usage == 'customer' and self.origin_returned_move_id:
debit_value = self.origin_returned_move_id.price_unit * qty
credit_value = debit_value
partner_id = (self.picking_id.partner_id and self.pool.get('res.partner')._find_accounting_partner(self.picking_id.partner_id).id) or False
debit_line_vals = {
'name': self.name,
'product_id': self.product_id.id,
'quantity': qty,
'product_uom_id': self.product_id.uom_id.id,
'ref': self.picking_id.name,
'partner_id': partner_id,
'debit': debit_value,
'credit': 0,
'account_id': debit_account_id,
}
credit_line_vals = {
'name': self.name,
'product_id': self.product_id.id,
'quantity': qty,
'product_uom_id': self.product_id.uom_id.id,
'ref': self.picking_id.name,
'partner_id': partner_id,
'credit': credit_value,
'debit': 0,
'account_id': credit_account_id,
}
res = [(0, 0, debit_line_vals), (0, 0, credit_line_vals)]
if credit_value != debit_value:
# for supplier returns of product in average costing method, in anglo saxon mode
diff_amount = debit_value - credit_value
price_diff_account = self.product_id.property_account_creditor_price_difference
if not price_diff_account:
price_diff_account = self.product_id.categ_id.property_account_creditor_price_difference_categ
if not price_diff_account:
raise UserError(_('Configuration error. Please configure the price difference account on the product or its category to process this operation.'))
price_diff_line = {
'name': self.name,
'product_id': self.product_id.id,
'quantity': qty,
'product_uom_id': self.product_id.uom_id.id,
'ref': self.picking_id.name,
'partner_id': partner_id,
'credit': diff_amount > 0 and diff_amount or 0,
'debit': diff_amount < 0 and -diff_amount or 0,
'account_id': price_diff_account.id,
}
res.append((0, 0, price_diff_line))
return res
@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class StockConfigSettings(models.TransientModel):
_inherit = 'stock.config.settings'
group_stock_inventory_valuation = fields.Selection([
(0, "Periodic inventory valuation (recommended)"),
(1, 'Perpetual inventory valuation (stock move generates accounting entries)')],
"Inventory Valuation", implied_group='stock_account.group_inventory_valuation',
help="""Allows to configure inventory valuations on products and product categories.""")
module_stock_landed_costs = fields.Selection([
(0, 'No landed costs'),
(1, 'Include landed costs in product costing computation')], "Landed Costs",
help="""Install the module that allows to affect landed costs on pickings, and split them onto the different products.""")
@api.onchange('module_stock_landed_costs')
def onchange_landed_costs(self):
if self.module_stock_landed_costs:
self.group_stock_inventory_valuation = 1
-276
View File
@@ -1,276 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp.osv import fields, osv
from openerp.tools.translate import _
from openerp import api
from openerp.exceptions import UserError
class product_template(osv.osv):
_name = 'product.template'
_inherit = 'product.template'
def _get_cost_method(self, cr, uid, ids, field, args, context=None):
res = {}
for product in self.browse(cr, uid, ids, context=context):
if product.property_cost_method:
res[product.id] = product.property_cost_method
else:
res[product.id] = product.categ_id.property_cost_method
return res
def _get_valuation_type(self, cr, uid, ids, field, args, context=None):
res = {}
for product in self.browse(cr, uid, ids, context=context):
if product.property_valuation:
res[product.id] = product.property_valuation
else:
res[product.id] = product.categ_id.property_valuation
return res
def _set_cost_method(self, cr, uid, ids, name, value, arg, context=None):
return self.write(cr, uid, ids, {'property_cost_method': value}, context=context)
def _set_valuation_type(self, cr, uid, ids, name, value, arg, context=None):
return self.write(cr, uid, ids, {'property_valuation': value}, context=context)
_columns = {
'property_valuation': fields.property(type='selection', selection=[('manual_periodic', 'Periodic (manual)'),
('real_time', 'Perpetual (automated)')], string='Inventory Valuation',
help="If perpetual valuation is enabled for a product, the system will automatically create journal entries corresponding to stock moves, with product price as specified by the 'Costing Method'" \
"The inventory variation account set on the product category will represent the current inventory value, and the stock input and stock output account will hold the counterpart moves for incoming and outgoing products."
, copy=True),
'valuation': fields.function(_get_valuation_type, fnct_inv=_set_valuation_type, type='char'), # TDE FIXME: store it ?
'property_cost_method': fields.property(type='selection', selection=[('standard', 'Standard Price'), ('average', 'Average Price'), ('real', 'Real Price')],
help="""Standard Price: The cost price is manually updated at the end of a specific period (usually once a year).
Average Price: The cost price is recomputed at each incoming shipment and used for the product valuation.
Real Price: The cost price displayed is the price of the last outgoing product (will be use in case of inventory loss for example).""",
string="Costing Method", copy=True),
'cost_method': fields.function(_get_cost_method, fnct_inv=_set_cost_method, type='char'), # TDE FIXME: store it ?
'property_stock_account_input': fields.property(
type='many2one',
relation='account.account',
string='Stock Input Account',
domain=[('deprecated', '=', False)],
help="When doing real-time inventory valuation, counterpart journal items for all incoming stock moves will be posted in this account, unless "
"there is a specific valuation account set on the source location. When not set on the product, the one from the product category is used."),
'property_stock_account_output': fields.property(
type='many2one',
relation='account.account',
string='Stock Output Account',
domain=[('deprecated', '=', False)],
help="When doing real-time inventory valuation, counterpart journal items for all outgoing stock moves will be posted in this account, unless "
"there is a specific valuation account set on the destination location. When not set on the product, the one from the product category is used."),
}
_defaults = {
'property_valuation': 'manual_periodic',
}
def create(self, cr, uid, vals, context=None):
if vals.get('cost_method'):
vals['property_cost_method'] = vals.pop('cost_method')
if vals.get('valuation'):
vals['property_valuation'] = vals.pop('valuation')
return super(product_template, self).create(cr, uid, vals, context=context)
@api.onchange('type')
def onchange_type_valuation(self):
if self.type != 'product':
self.valuation = 'manual_periodic'
return {}
@api.multi
def _get_product_accounts(self):
""" Add the stock accounts related to product to the result of super()
@return: dictionary which contains information regarding stock accounts and super (income+expense accounts)
"""
accounts = super(product_template, self)._get_product_accounts()
res = self._get_asset_accounts()
accounts.update({
'stock_input': res['stock_input'] or self.property_stock_account_input or self.categ_id.property_stock_account_input_categ_id,
'stock_output': res['stock_output'] or self.property_stock_account_output or self.categ_id.property_stock_account_output_categ_id,
'stock_valuation': self.categ_id.property_stock_valuation_account_id or False,
})
return accounts
@api.multi
def get_product_accounts(self, fiscal_pos=None):
""" Add the stock journal related to product to the result of super()
@return: dictionary which contains all needed information regarding stock accounts and journal and super (income+expense accounts)
"""
accounts = super(product_template, self).get_product_accounts(fiscal_pos=fiscal_pos)
accounts.update({'stock_journal': self.categ_id.property_stock_journal or False})
return accounts
# To remove in master because this function is now used on "product.product" model.
def do_change_standard_price(self, cr, uid, ids, new_price, account_id, context=None):
""" Changes the Standard Price of Product and creates an account move accordingly."""
location_obj = self.pool.get('stock.location')
move_obj = self.pool.get('account.move')
if context is None:
context = {}
user_company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
loc_ids = location_obj.search(cr, uid, [('usage', '=', 'internal'), ('company_id', '=', user_company_id)])
for rec_id in ids:
datas = self.get_product_accounts(cr, uid, rec_id, context=context)
for location in location_obj.browse(cr, uid, loc_ids, context=context):
c = context.copy()
c.update({'location': location.id, 'compute_child': False})
product = self.browse(cr, uid, rec_id, context=c)
diff = product.standard_price - new_price
if not diff:
raise UserError(_("No difference between standard price and new price!"))
for prod_variant in product.product_variant_ids:
qty = prod_variant.qty_available
if qty:
# Accounting Entries
amount_diff = abs(diff * qty)
if diff * qty > 0:
debit_account_id = account_id
credit_account_id = datas['stock_valuation'].id
else:
debit_account_id = datas['stock_valuation'].id
credit_account_id = account_id
lines = [(0, 0, {'name': _('Standard Price changed'),
'account_id': debit_account_id,
'debit': amount_diff,
'credit': 0,
'product_id': product.id,
}),
(0, 0, {
'name': _('Standard Price changed'),
'account_id': credit_account_id,
'debit': 0,
'credit': amount_diff,
'product_id': product.id,
})]
move_vals = {
'journal_id': datas['stock_journal'].id,
'company_id': location.company_id.id,
'line_ids': lines,
}
move_id = move_obj.create(cr, uid, move_vals, context=context)
move_obj.post(cr, uid, [move_id], context=context)
self.write(cr, uid, rec_id, {'standard_price': new_price})
return True
class product_product(osv.osv):
_inherit = 'product.product'
@api.onchange('type')
def onchange_type_valuation(self):
if self.type != 'product':
self.valuation = 'manual_periodic'
return {}
def do_change_standard_price(self, cr, uid, ids, new_price, account_id, context=None):
""" Changes the Standard Price of Product and creates an account move accordingly."""
location_obj = self.pool.get('stock.location')
move_obj = self.pool.get('account.move')
if context is None:
context = {}
user_company_id = self.pool.get('res.users').browse(cr, uid, uid, context=context).company_id.id
loc_ids = location_obj.search(cr, uid, [('usage', '=', 'internal'), ('company_id', '=', user_company_id)])
for rec_id in ids:
for location in location_obj.browse(cr, uid, loc_ids, context=context):
c = context.copy()
c.update({'location': location.id, 'compute_child': False})
product = self.browse(cr, uid, rec_id, context=c)
datas = self.pool['product.template'].get_product_accounts(cr, uid, product.product_tmpl_id.id, context=context)
diff = product.standard_price - new_price
if not diff:
raise UserError(_("No difference between standard price and new price!"))
qty = product.qty_available
if qty:
# Accounting Entries
amount_diff = abs(diff * qty)
if diff * qty > 0:
debit_account_id = account_id
credit_account_id = datas['stock_valuation'].id
else:
debit_account_id = datas['stock_valuation'].id
credit_account_id = account_id
lines = [(0, 0, {'name': _('Standard Price changed'),
'account_id': debit_account_id,
'debit': amount_diff,
'credit': 0,
}),
(0, 0, {
'name': _('Standard Price changed'),
'account_id': credit_account_id,
'debit': 0,
'credit': amount_diff,
})]
move_vals = {
'journal_id': datas['stock_journal'].id,
'company_id': location.company_id.id,
'line_ids': lines,
}
move_id = move_obj.create(cr, uid, move_vals, context=context)
move_obj.post(cr, uid, [move_id], context=context)
self.write(cr, uid, rec_id, {'standard_price': new_price})
return True
class product_category(osv.osv):
_inherit = 'product.category'
_columns = {
'property_valuation': fields.property(
type='selection',
selection=[('manual_periodic', 'Periodic (manual)'),
('real_time', 'Perpetual (automated)')],
string='Inventory Valuation',
required=True, copy=True,
help="If perpetual valuation is enabled for a product, the system "
"will automatically create journal entries corresponding to "
"stock moves, with product price as specified by the 'Costing "
"Method'. The inventory variation account set on the product "
"category will represent the current inventory value, and the "
"stock input and stock output account will hold the counterpart "
"moves for incoming and outgoing products."),
'property_cost_method': fields.property(
type='selection',
selection=[('standard', 'Standard Price'),
('average', 'Average Price'),
('real', 'Real Price')],
string="Costing Method",
required=True, copy=True,
help="Standard Price: The cost price is manually updated at the end "
"of a specific period (usually once a year).\nAverage Price: "
"The cost price is recomputed at each incoming shipment and "
"used for the product valuation.\nReal Price: The cost price "
"displayed is the price of the last outgoing product (will be "
"used in case of inventory loss for example)."""),
'property_stock_journal': fields.property(
relation='account.journal',
type='many2one',
string='Stock Journal',
help="When doing real-time inventory valuation, this is the Accounting Journal in which entries will be automatically posted when stock moves are processed."),
'property_stock_account_input_categ_id': fields.property(
type='many2one',
relation='account.account',
string='Stock Input Account',
domain=[('deprecated', '=', False)], oldname="property_stock_account_input_categ",
help="When doing real-time inventory valuation, counterpart journal items for all incoming stock moves will be posted in this account, unless "
"there is a specific valuation account set on the source location. This is the default value for all products in this category. It "
"can also directly be set on each product"),
'property_stock_account_output_categ_id': fields.property(
type='many2one',
relation='account.account',
domain=[('deprecated', '=', False)],
string='Stock Output Account', oldname="property_stock_account_output_categ",
help="When doing real-time inventory valuation, counterpart journal items for all outgoing stock moves will be posted in this account, unless "
"there is a specific valuation account set on the destination location. This is the default value for all products in this category. It "
"can also directly be set on each product"),
'property_stock_valuation_account_id': fields.property(
type='many2one',
relation='account.account',
string="Stock Valuation Account",
domain=[('deprecated', '=', False)],
help="When real-time inventory valuation is enabled on a product, this account will hold the current value of the products.",),
}
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import stock_history
@@ -0,0 +1,157 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, tools
class StockHistory(models.Model):
_name = 'stock.history'
_auto = False
_order = 'date asc'
move_id = fields.Many2one('stock.move', 'Stock Move', required=True)
location_id = fields.Many2one('stock.location', 'Location', required=True)
company_id = fields.Many2one('res.company', 'Company')
product_id = fields.Many2one('product.product', 'Product', required=True)
product_categ_id = fields.Many2one('product.category', 'Product Category', required=True)
quantity = fields.Float('Product Quantity')
date = fields.Datetime('Operation Date')
price_unit_on_quant = fields.Float('Value')
inventory_value = fields.Float('Inventory Value', compute='_compute_inventory_value', readonly=True)
source = fields.Char('Source')
product_template_id = fields.Many2one('product.template', 'Product Template', required=True)
serial_number = fields.Char('Lot/Serial Number', required=True)
@api.model
def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
res = super(StockHistory, self).read_group(domain, fields, groupby, offset=offset, limit=limit, orderby=orderby, lazy=lazy)
if 'inventory_value' in fields:
date = self._context.get('history_date', fields.Datetime.now())
stock_history = self.env['stock.history']
group_lines = {}
for line in res:
domain = line.get('__domain', domain)
group_lines.setdefault(str(domain), self.search(domain))
stock_history |= group_lines[str(domain)]
histories_dict = {}
not_real_cost_method_products = stock_history.mapped('product_id').filtered(lambda product: product.cost_method != 'real')
if not_real_cost_method_products:
self._cr.execute("""SELECT DISTINCT ON (product_id, company_id) product_id, company_id, cost
FROM product_price_history
WHERE product_id in %s AND datetime <= %s
ORDER BY product_id, company_id, datetime DESC""", (not_real_cost_method_products.ids, date))
for history in self._cr.dictfetchall():
histories_dict[(history['product_id'], history['company_id'])] = history['cost']
for line in res:
inv_value = 0.0
for stock_history in group_lines.get(str(line.get('__domain', domain))):
product = stock_history.product_id
if product.cost_method == 'real':
price = stock_history.price_unit_on_quant
else:
price = histories_dict.get((product.id, stock_history.company_id.id), 0.0)
inv_value += price * stock_history.quantity
line['inventory_value'] = inv_value
return res
@api.one
def _compute_inventory_value(self):
if self.product_id.cost_method == 'real':
self.inventory_value = self.quantity * self.price_unit_on_quant
else:
self.inventory_value = self.quantity * self.product_id.get_history_price(self.company_id.id, date=self._context.get('history_date', fields.Datetime.now()))
@api.model_cr
def init(self):
tools.drop_view_if_exists(self._cr, 'stock_history')
self._cr.execute("""
CREATE VIEW stock_history AS (
SELECT MIN(id) as id,
move_id,
location_id,
company_id,
product_id,
product_categ_id,
product_template_id,
SUM(quantity) as quantity,
date,
SUM(price_unit_on_quant * quantity) / SUM(quantity) as price_unit_on_quant,
source,
string_agg(DISTINCT serial_number, ', ' ORDER BY serial_number) AS serial_number
FROM
((SELECT
stock_move.id AS id,
stock_move.id AS move_id,
dest_location.id AS location_id,
dest_location.company_id AS company_id,
stock_move.product_id AS product_id,
product_template.id AS product_template_id,
product_template.categ_id AS product_categ_id,
quant.qty AS quantity,
stock_move.date AS date,
quant.cost as price_unit_on_quant,
stock_move.origin AS source,
stock_production_lot.name AS serial_number
FROM
stock_quant as quant
JOIN
stock_quant_move_rel ON stock_quant_move_rel.quant_id = quant.id
JOIN
stock_move ON stock_move.id = stock_quant_move_rel.move_id
LEFT JOIN
stock_production_lot ON stock_production_lot.id = quant.lot_id
JOIN
stock_location dest_location ON stock_move.location_dest_id = dest_location.id
JOIN
stock_location source_location ON stock_move.location_id = source_location.id
JOIN
product_product ON product_product.id = stock_move.product_id
JOIN
product_template ON product_template.id = product_product.product_tmpl_id
WHERE quant.qty>0 AND stock_move.state = 'done' AND dest_location.usage in ('internal', 'transit')
AND (
not (source_location.company_id is null and dest_location.company_id is null) or
source_location.company_id != dest_location.company_id or
source_location.usage not in ('internal', 'transit'))
) UNION ALL
(SELECT
(-1) * stock_move.id AS id,
stock_move.id AS move_id,
source_location.id AS location_id,
source_location.company_id AS company_id,
stock_move.product_id AS product_id,
product_template.id AS product_template_id,
product_template.categ_id AS product_categ_id,
- quant.qty AS quantity,
stock_move.date AS date,
quant.cost as price_unit_on_quant,
stock_move.origin AS source,
stock_production_lot.name AS serial_number
FROM
stock_quant as quant
JOIN
stock_quant_move_rel ON stock_quant_move_rel.quant_id = quant.id
JOIN
stock_move ON stock_move.id = stock_quant_move_rel.move_id
LEFT JOIN
stock_production_lot ON stock_production_lot.id = quant.lot_id
JOIN
stock_location source_location ON stock_move.location_id = source_location.id
JOIN
stock_location dest_location ON stock_move.location_dest_id = dest_location.id
JOIN
product_product ON product_product.id = stock_move.product_id
JOIN
product_template ON product_template.id = product_product.product_tmpl_id
WHERE quant.qty>0 AND stock_move.state = 'done' AND source_location.usage in ('internal', 'transit')
AND (
not (dest_location.company_id is null and source_location.company_id is null) or
dest_location.company_id != source_location.company_id or
dest_location.usage not in ('internal', 'transit'))
))
AS foo
GROUP BY move_id, location_id, company_id, product_id, product_categ_id, date, source, product_template_id
)""")
@@ -1,39 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data>
<record id="view_wizard_valuation_history" model="ir.ui.view">
<field name="name">Inventory at Date</field>
<field name="model">wizard.valuation.history</field>
<field name="arch" type="xml">
<form string="Choose your date">
<p class="oe_gray" attrs="{'invisible': [('choose_date', '=', False)]}">
Choose a date in the past to get the inventory at that date.
</p>
<p class="oe_gray" attrs="{'invisible': [('choose_date', '=', True)]}">
Retrieve the curent stock valuation.
</p>
<group>
<field name="choose_date"/>
<field name="date" attrs="{'invisible': [('choose_date', '=', False)]}"/>
</group>
<footer>
<button name="open_table" string="Retrieve the Inventory Value" type="object" class="btn-primary"/>
<button string="Cancel" class="btn-default" special="cancel" />
</footer>
</form>
</field>
</record>
<record id="action_wizard_stock_valuation_history" model="ir.actions.act_window">
<field name="name">Inventory at Date</field>
<field name="res_model">wizard.valuation.history</field>
<field name="view_type">form</field>
<field name="view_mode">form</field>
<field name="view_id" ref="view_wizard_valuation_history"/>
<field name="target">new</field>
</record>
<menuitem id="menu_action_wizard_valuation_history" action="action_wizard_stock_valuation_history" parent="stock.menu_warehouse_report" sequence="1" groups="base.group_no_one"/>
<odoo>
<record id="view_stock_history_report_tree" model="ir.ui.view">
<field name="name">Stock Value At Date</field>
<field name="model">stock.history</field>
@@ -92,6 +58,4 @@
</search>
</field>
</record>
</data>
</openerp>
</odoo>
-29
View File
@@ -1,29 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp.osv import fields, osv
class stock_config_settings(osv.osv_memory):
_inherit = 'stock.config.settings'
_columns = {
'group_stock_inventory_valuation': fields.selection([
(0, "Periodic inventory valuation (recommended)"),
(1, 'Perpetual inventory valuation (stock move generates accounting entries)')
], "Inventory Valuation",
implied_group='stock_account.group_inventory_valuation',
help="""Allows to configure inventory valuations on products and product categories."""),
'module_stock_landed_costs': fields.selection([
(0, 'No landed costs'),
(1, 'Include landed costs in product costing computation')
], "Landed Costs",
help="""Install the module that allows to affect landed costs on pickings, and split them onto the different products."""),
}
def onchange_landed_costs(self, cr, uid, ids, module_landed_costs, context=None):
if module_landed_costs:
return {'value': {'group_stock_inventory_valuation': True}}
return {}
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<odoo>
<data noupdate="0">
<record id="stock.group_stock_manager" model="res.groups">
<field name="implied_ids" eval="[(4, ref('account.group_account_invoice'))]"/>
@@ -18,5 +18,5 @@
</record>
</data>
</openerp>
</odoo>
-518
View File
@@ -1,518 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp.osv import fields, osv
from openerp.tools import float_compare, float_round
from openerp.tools.translate import _
from openerp import SUPERUSER_ID, api, models
from openerp.exceptions import UserError
import logging
_logger = logging.getLogger(__name__)
class stock_inventory(osv.osv):
_inherit = "stock.inventory"
_columns = {
'accounting_date': fields.date('Force Accounting Date', help="Choose the accounting date at which you want to value the stock moves created by the inventory instead of the default one (the inventory end date)"),
}
def post_inventory(self, cr, uid, ids, context=None):
if context is None:
context = {}
ctx = context.copy()
inv = self.browse(cr, uid, ids, context=context)[0]
if inv.accounting_date:
ctx['force_period_date'] = inv.accounting_date
return super(stock_inventory, self).post_inventory(cr, uid, ids, context=ctx)
class account_invoice_line(osv.osv):
_inherit = "account.invoice.line"
def _get_anglo_saxon_price_unit(self):
self.ensure_one()
return self.product_id.standard_price
def _get_price(self, cr, uid, inv, company_currency, i_line, price_unit):
cur_obj = self.pool.get('res.currency')
if inv.currency_id.id != company_currency:
price = cur_obj.compute(cr, uid, company_currency, inv.currency_id.id, price_unit * i_line.quantity, context={'date': inv.date_invoice})
else:
price = price_unit * i_line.quantity
return round(price, inv.currency_id.decimal_places)
def get_invoice_line_account(self, type, product, fpos, company):
if company.anglo_saxon_accounting and type in ('in_invoice', 'in_refund') and product and product.type == 'product':
accounts = product.product_tmpl_id.get_product_accounts(fiscal_pos=fpos)
return accounts['stock_input']
return super(account_invoice_line, self).get_invoice_line_account(type, product, fpos, company)
class account_invoice(osv.osv):
_inherit = "account.invoice"
@api.model
def invoice_line_move_line_get(self):
res = super(account_invoice,self).invoice_line_move_line_get()
if self.company_id.anglo_saxon_accounting:
if self.type in ('out_invoice','out_refund'):
for i_line in self.invoice_line_ids:
res.extend(self._anglo_saxon_sale_move_lines(i_line))
return res
@api.model
def _anglo_saxon_sale_move_lines(self, i_line):
"""Return the additional move lines for sales 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.id
if i_line.product_id.type == 'product' and i_line.product_id.valuation == 'real_time':
fpos = i_line.invoice_id.fiscal_position_id
accounts = i_line.product_id.product_tmpl_id.get_product_accounts(fiscal_pos=fpos)
# debit account dacc will be the output account
dacc = accounts['stock_output'].id
# credit account cacc will be the expense account
cacc = accounts['expense'].id
if dacc and cacc:
price_unit = i_line._get_anglo_saxon_price_unit()
return [
{
'type':'src',
'name': i_line.name[:64],
'price_unit': price_unit,
'quantity': i_line.quantity,
'price': self.env['account.invoice.line']._get_price(inv, company_currency, i_line, price_unit),
'account_id':dacc,
'product_id':i_line.product_id.id,
'uom_id':i_line.uom_id.id,
'account_analytic_id': False,
},
{
'type':'src',
'name': i_line.name[:64],
'price_unit': price_unit,
'quantity': i_line.quantity,
'price': -1 * self.env['account.invoice.line']._get_price(inv, company_currency, i_line, price_unit),
'account_id':cacc,
'product_id':i_line.product_id.id,
'uom_id':i_line.uom_id.id,
'account_analytic_id': False,
},
]
return []
#----------------------------------------------------------
# Stock Location
#----------------------------------------------------------
class stock_location(osv.osv):
_inherit = "stock.location"
_columns = {
'valuation_in_account_id': fields.many2one('account.account', 'Stock Valuation Account (Incoming)', domain=[('internal_type', '=', 'other'), ('deprecated', '=', False)],
help="Used for real-time inventory valuation. When set on a virtual location (non internal type), "
"this account will be used to hold the value of products being moved from an internal location "
"into this location, instead of the generic Stock Output Account set on the product. "
"This has no effect for internal locations."),
'valuation_out_account_id': fields.many2one('account.account', 'Stock Valuation Account (Outgoing)', domain=[('internal_type', '=', 'other'), ('deprecated', '=', False)],
help="Used for real-time inventory valuation. When set on a virtual location (non internal type), "
"this account will be used to hold the value of products being moved out of this location "
"and into an internal location, instead of the generic Stock Output Account set on the product. "
"This has no effect for internal locations."),
}
#----------------------------------------------------------
# Quants
#----------------------------------------------------------
class stock_quant(osv.osv):
_inherit = "stock.quant"
@api.multi
def _compute_inventory_value(self):
real_value_quants = self.filtered(lambda quant: quant.product_id.cost_method == 'real')
for quant in real_value_quants:
quant.inventory_value = quant.cost * quant.qty
other = self - real_value_quants
return super(stock_quant, other)._compute_inventory_value()
@api.cr_uid_ids_context
def _price_update(self, cr, uid, quant_ids, newprice, context=None):
''' This function is called at the end of negative quant reconciliation and does the accounting entries adjustemnts and the update of the product cost price if needed
'''
if context is None:
context = {}
account_move_obj = self.pool['account.move']
super(stock_quant, self)._price_update(cr, uid, quant_ids, newprice, context=context)
for quant in self.browse(cr, uid, quant_ids, context=context):
move = quant._get_latest_move()
valuation_update = newprice - quant.cost
# this is where we post accounting entries for adjustment, if needed
if not quant.company_id.currency_id.is_zero(valuation_update):
# If neg quant period already closed (likely with manual valuation), skip update
if account_move_obj._check_lock_date(cr, uid, [move.id], context=context):
ctx = dict(context, force_valuation_amount=valuation_update)
self._account_entry_move(cr, uid, [quant], move, context=ctx)
#update the standard price of the product, only if we would have done it if we'd have had enough stock at first, which means
#1) the product cost's method is 'real'
#2) we just fixed a negative quant caused by an outgoing shipment
if quant.product_id.cost_method == 'real' and quant.location_id.usage != 'internal':
self.pool.get('stock.move')._store_average_cost_price(cr, uid, move, context=context)
def _account_entry_move(self, cr, uid, ids, move, context=None):
"""
Accounting Valuation Entries
quants: browse record list of Quants to create accounting valuation entries for. Unempty and all quants are supposed to have the same location id (thay already moved in)
move: Move to use. browse record
"""
quants = self.browse(cr, uid, ids, context=context)
if context is None:
context = {}
location_obj = self.pool.get('stock.location')
location_from = move.location_id
location_to = quants[0].location_id
company_from = location_from and (location_from.usage == 'internal') and location_from.company_id or False
company_to = location_to and (location_to.usage == 'internal') and location_to.company_id or False
if move.product_id.valuation != 'real_time':
return False
if move.product_id.type != 'product':
#No stock valuation for consumable products
return False
for q in quants:
if q.owner_id:
#if the quant isn't owned by the company, we don't make any valuation entry
return False
if q.qty <= 0:
#we don't make any stock valuation for negative quants because the valuation is already made for the counterpart.
#At that time the valuation will be made at the product cost price and afterward there will be new accounting entries
#to make the adjustments when we know the real cost price.
return False
#in case of routes making the link between several warehouse of the same company, the transit location belongs to this company, so we don't need to create accounting entries
# Create Journal Entry for products arriving in the company
if company_to and (move.location_id.usage not in ('internal', 'transit') and move.location_dest_id.usage == 'internal' or company_from != company_to):
ctx = context.copy()
ctx['force_company'] = company_to.id
journal_id, acc_src, acc_dest, acc_valuation = self._get_accounting_data_for_valuation(cr, uid, move, context=ctx)
if location_from and location_from.usage == 'customer':
#goods returned from customer
self._create_account_move_line(cr, uid, quants, move, acc_dest, acc_valuation, journal_id, context=ctx)
else:
self._create_account_move_line(cr, uid, quants, move, acc_src, acc_valuation, journal_id, context=ctx)
# Create Journal Entry for products leaving the company
if company_from and (move.location_id.usage == 'internal' and move.location_dest_id.usage not in ('internal', 'transit') or company_from != company_to):
ctx = context.copy()
ctx['force_company'] = company_from.id
journal_id, acc_src, acc_dest, acc_valuation = self._get_accounting_data_for_valuation(cr, uid, move, context=ctx)
if location_to and location_to.usage == 'supplier':
#goods returned to supplier
self._create_account_move_line(cr, uid, quants, move, acc_valuation, acc_src, journal_id, context=ctx)
else:
self._create_account_move_line(cr, uid, quants, move, acc_valuation, acc_dest, journal_id, context=ctx)
def _quant_create_from_move(self, cr, uid, qty, move, lot_id=False, owner_id=False, src_package_id=False, dest_package_id=False, force_location_from=False, force_location_to=False, context=None):
quant_obj = self.pool.get('stock.quant')
quant = super(stock_quant, self)._quant_create_from_move(cr, uid, qty, move, lot_id=lot_id, owner_id=owner_id, src_package_id=src_package_id, dest_package_id=dest_package_id, force_location_from=force_location_from, force_location_to=force_location_to, context=context)
quant._account_entry_move(move)
if move.product_id.valuation == 'real_time':
# If the precision required for the variable quant cost is larger than the accounting
# precision, inconsistencies between the stock valuation and the accounting entries
# may arise.
# For example, a box of 13 units is bought 15.00. If the products leave the
# stock one unit at a time, the amount related to the cost will correspond to
# round(15/13, 2)*13 = 14.95. To avoid this case, we split the quant in 12 + 1, then
# record the difference on the new quant.
# We need to make sure to able to extract at least one unit of the product. There is
# an arbitrary minimum quantity set to 2.0 from which we consider we can extract a
# unit and adapt the cost.
curr_rounding = move.company_id.currency_id.rounding
cost_rounded = float_round(quant.cost, precision_rounding=curr_rounding)
cost_correct = cost_rounded
if float_compare(quant.product_id.uom_id.rounding, 1.0, precision_digits=1) == 0\
and float_compare(quant.qty * quant.cost, quant.qty * cost_rounded, precision_rounding=curr_rounding) != 0\
and float_compare(quant.qty, 2.0, precision_rounding=quant.product_id.uom_id.rounding) >= 0:
qty = quant.qty
cost = quant.cost
quant_correct = quant._quant_split(quant.qty - 1.0)
cost_correct += (qty * cost) - (qty * cost_rounded)
quant_obj.write(cr, SUPERUSER_ID, [quant.id], {'cost': cost_rounded}, context=context)
quant_obj.write(cr, SUPERUSER_ID, [quant_correct.id], {'cost': cost_correct}, context=context)
return quant
@api.multi
def _quant_update_from_move(self, move, location_dest_id, dest_package_id, lot_id=False, entire_pack=False):
res = super(stock_quant, self)._quant_update_from_move(move, location_dest_id, dest_package_id, lot_id=lot_id, entire_pack=entire_pack)
self._account_entry_move(move)
return res
def _get_accounting_data_for_valuation(self, cr, uid, move, context=None):
"""
Return the accounts and journal to use to post Journal Entries for the real-time
valuation of the quant.
:param context: context dictionary that can explicitly mention the company to consider via the 'force_company' key
:returns: journal_id, source account, destination account, valuation account
:raise: openerp.exceptions.UserError if any mandatory account or journal is not defined.
"""
product_obj = self.pool.get('product.template')
accounts = product_obj.browse(cr, uid, move.product_id.product_tmpl_id.id, context).get_product_accounts()
if move.location_id.valuation_out_account_id:
acc_src = move.location_id.valuation_out_account_id.id
else:
acc_src = accounts['stock_input'].id
if move.location_dest_id.valuation_in_account_id:
acc_dest = move.location_dest_id.valuation_in_account_id.id
else:
acc_dest = accounts['stock_output'].id
acc_valuation = accounts.get('stock_valuation', False)
if acc_valuation:
acc_valuation = acc_valuation.id
if not accounts.get('stock_journal', False):
raise UserError(_('You don\'t have any stock journal defined on your product category, check if you have installed a chart of accounts'))
if not acc_src:
raise UserError(_('Cannot find a stock input account for the product %s. You must define one on the product category, or on the location, before processing this operation.') % (move.product_id.name))
if not acc_dest:
raise UserError(_('Cannot find a stock output account for the product %s. You must define one on the product category, or on the location, before processing this operation.') % (move.product_id.name))
if not acc_valuation:
raise UserError(_('You don\'t have any stock valuation account defined on your product category. You must define one before processing this operation.'))
journal_id = accounts['stock_journal'].id
return journal_id, acc_src, acc_dest, acc_valuation
def _prepare_account_move_line(self, cr, uid, move, qty, cost, credit_account_id, debit_account_id, context=None):
"""
Generate the account.move.line values to post to track the stock valuation difference due to the
processing of the given quant.
"""
if context is None:
context = {}
currency_obj = self.pool.get('res.currency')
if context.get('force_valuation_amount'):
valuation_amount = context.get('force_valuation_amount')
else:
if move.product_id.cost_method == 'average':
valuation_amount = cost if move.location_id.usage == 'supplier' and move.location_dest_id.usage == 'internal' else move.product_id.standard_price
else:
valuation_amount = cost if move.product_id.cost_method == 'real' else move.product_id.standard_price
#the standard_price of the product may be in another decimal precision, or not compatible with the coinage of
#the company currency... so we need to use round() before creating the accounting entries.
debit_value = currency_obj.round(cr, uid, move.company_id.currency_id, valuation_amount * qty)
#check that all data is correct
if move.company_id.currency_id.is_zero(debit_value):
raise UserError(_("The found valuation amount for product %s is zero. Which means there is probably a configuration error. Check the costing method and the standard price") % (move.product_id.name,))
credit_value = debit_value
if move.product_id.cost_method == 'average' and move.company_id.anglo_saxon_accounting:
#in case of a supplier return in anglo saxon mode, for products in average costing method, the stock_input
#account books the real purchase price, while the stock account books the average price. The difference is
#booked in the dedicated price difference account.
if move.location_dest_id.usage == 'supplier' and move.origin_returned_move_id and move.origin_returned_move_id.purchase_line_id:
debit_value = move.origin_returned_move_id.price_unit * qty
#in case of a customer return in anglo saxon mode, for products in average costing method, the stock valuation
#is made using the original average price to negate the delivery effect.
if move.location_id.usage == 'customer' and move.origin_returned_move_id:
debit_value = move.origin_returned_move_id.price_unit * qty
credit_value = debit_value
partner_id = (move.picking_id.partner_id and self.pool.get('res.partner')._find_accounting_partner(move.picking_id.partner_id).id) or False
debit_line_vals = {
'name': move.name,
'product_id': move.product_id.id,
'quantity': qty,
'product_uom_id': move.product_id.uom_id.id,
'ref': move.picking_id and move.picking_id.name or False,
'partner_id': partner_id,
'debit': debit_value,
'credit': 0,
'account_id': debit_account_id,
}
credit_line_vals = {
'name': move.name,
'product_id': move.product_id.id,
'quantity': qty,
'product_uom_id': move.product_id.uom_id.id,
'ref': move.picking_id and move.picking_id.name or False,
'partner_id': partner_id,
'credit': credit_value,
'debit': 0,
'account_id': credit_account_id,
}
res = [(0, 0, debit_line_vals), (0, 0, credit_line_vals)]
if credit_value != debit_value:
#for supplier returns of product in average costing method, in anglo saxon mode
diff_amount = debit_value - credit_value
price_diff_account = move.product_id.property_account_creditor_price_difference
if not price_diff_account:
price_diff_account = move.product_id.categ_id.property_account_creditor_price_difference_categ
if not price_diff_account:
raise UserError(_('Configuration error. Please configure the price difference account on the product or its category to process this operation.'))
price_diff_line = {
'name': move.name,
'product_id': move.product_id.id,
'quantity': qty,
'product_uom_id': move.product_id.uom_id.id,
'ref': move.picking_id and move.picking_id.name or False,
'partner_id': partner_id,
'credit': diff_amount > 0 and diff_amount or 0,
'debit': diff_amount < 0 and -diff_amount or 0,
'account_id': price_diff_account.id,
}
res.append((0, 0, price_diff_line))
return res
def _create_account_move_line(self, cr, uid, quants, move, credit_account_id, debit_account_id, journal_id, context=None):
#group quants by cost
quant_cost_qty = {}
for quant in quants:
if quant_cost_qty.get(quant.cost):
quant_cost_qty[quant.cost] += quant.qty
else:
quant_cost_qty[quant.cost] = quant.qty
move_obj = self.pool.get('account.move')
for cost, qty in quant_cost_qty.items():
move_lines = self._prepare_account_move_line(cr, uid, move, qty, cost, credit_account_id, debit_account_id, context=context)
date = context.get('force_period_date', fields.date.context_today(self, cr, uid, context=context))
new_move = move_obj.create(cr, uid, {'journal_id': journal_id,
'line_ids': move_lines,
'date': date,
'ref': move.picking_id.name}, context=context)
move_obj.post(cr, uid, [new_move], context=context)
#def _reconcile_single_negative_quant(self, cr, uid, to_solve_quant, quant, quant_neg, qty, context=None):
# move = self._get_latest_move(cr, uid, to_solve_quant, context=context)
# quant_neg_position = quant_neg.negative_dest_location_id.usage
# remaining_solving_quant, remaining_to_solve_quant = super(stock_quant, self)._reconcile_single_negative_quant(cr, uid, to_solve_quant, quant, quant_neg, qty, context=context)
# #update the standard price of the product, only if we would have done it if we'd have had enough stock at first, which means
# #1) there isn't any negative quant anymore
# #2) the product cost's method is 'real'
# #3) we just fixed a negative quant caused by an outgoing shipment
# if not remaining_to_solve_quant and move.product_id.cost_method == 'real' and quant_neg_position != 'internal':
# self.pool.get('stock.move')._store_average_cost_price(cr, uid, move, context=context)
# return remaining_solving_quant, remaining_to_solve_quant
class stock_move(osv.osv):
_inherit = "stock.move"
def action_done(self, cr, uid, ids, context=None):
self.product_price_update_before_done(cr, uid, ids, context=context)
res = super(stock_move, self).action_done(cr, uid, ids, context=context)
self.product_price_update_after_done(cr, uid, ids, context=context)
return res
def _store_average_cost_price(self, cr, uid, move, context=None):
''' move is a browe record '''
product_obj = self.pool.get('product.product')
if any([q.qty <= 0 for q in move.quant_ids]) or move.product_qty == 0:
#if there is a negative quant, the standard price shouldn't be updated
return
#Note: here we can't store a quant.cost directly as we may have moved out 2 units (1 unit to 5€ and 1 unit to 7€) and in case of a product return of 1 unit, we can't know which of the 2 costs has to be used (5€ or 7€?). So at that time, thanks to the average valuation price we are storing we will valuate it at 6€
average_valuation_price = 0.0
for q in move.quant_ids:
average_valuation_price += q.qty * q.cost
average_valuation_price = average_valuation_price / move.product_qty
# Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products
ctx = dict(context or {}, force_company=move.company_id.id)
product_obj.write(cr, SUPERUSER_ID, [move.product_id.id], {'standard_price': average_valuation_price}, context=ctx)
self.write(cr, uid, [move.id], {'price_unit': average_valuation_price}, context=context)
def product_price_update_before_done(self, cr, uid, ids, context=None):
product_obj = self.pool.get('product.product')
tmpl_dict = {}
for move in self.browse(cr, uid, ids, context=context):
#adapt standard price on incomming moves if the product cost_method is 'average'
if (move.location_id.usage == 'supplier') and (move.product_id.cost_method == 'average'):
product = move.product_id
product_id = move.product_id.id
qty_available = move.product_id.qty_available
if tmpl_dict.get(product_id):
product_avail = qty_available + tmpl_dict[product_id]
else:
tmpl_dict[product_id] = 0
product_avail = qty_available
# if the incoming move is for a purchase order with foreign currency, need to call this to get the same value that the quant will use.
price_unit = move.get_price_unit()
if product_avail <= 0:
new_std_price = price_unit
else:
# Get the standard price
amount_unit = product.standard_price
new_std_price = ((amount_unit * product_avail) + (price_unit * move.product_qty)) / (product_avail + move.product_qty)
tmpl_dict[product_id] += move.product_qty
# Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products
ctx = dict(context or {}, force_company=move.company_id.id)
product_obj.write(cr, SUPERUSER_ID, [product.id], {'standard_price': new_std_price}, context=ctx)
def product_price_update_after_done(self, cr, uid, ids, context=None):
'''
This method adapts the price on the product when necessary
'''
for move in self.browse(cr, uid, ids, context=context):
#adapt standard price on outgoing moves if the product cost_method is 'real', so that a return
#or an inventory loss is made using the last value used for an outgoing valuation.
if move.product_id.cost_method == 'real' and move.location_dest_id.usage != 'internal':
#store the average price of the move on the move and product form
self._store_average_cost_price(cr, uid, move, context=context)
class AccountChartTemplate(models.Model):
_inherit = "account.chart.template"
@api.model
def generate_journals(self, acc_template_ref, company, journals_dict=None):
journal_to_add = [{'name': _('Stock Journal'), 'type': 'general', 'code': 'STJ', 'favorite': False, 'sequence': 8}]
super(AccountChartTemplate, self).generate_journals(acc_template_ref=acc_template_ref, company=company, journals_dict=journal_to_add)
@api.multi
def generate_properties(self, acc_template_ref, company, property_list=None):
super(AccountChartTemplate, self).generate_properties(acc_template_ref=acc_template_ref, company=company)
PropertyObj = self.env['ir.property'] # Property Stock Journal
value = self.env['account.journal'].search([('company_id', '=', company.id), ('code', '=', 'STJ'), ('type', '=', 'general')], limit=1)
if value:
field = self.env['ir.model.fields'].search([('name', '=', 'property_stock_journal'), ('model', '=', 'product.category'), ('relation', '=', 'account.journal')], limit=1)
vals = {
'name': 'property_stock_journal',
'company_id': company.id,
'fields_id': field.id,
'value': 'account.journal,%s' % value.id,
}
properties = PropertyObj.search([('name', '=', 'property_stock_journal'), ('company_id', '=', company.id)])
if properties:
#the property exist: modify it
properties.write(vals)
else:
#create the property
PropertyObj.create(vals)
todo_list = [ # Property Stock Accounts
'property_stock_account_input_categ_id',
'property_stock_account_output_categ_id',
'property_stock_valuation_account_id',
]
for record in todo_list:
account = getattr(self, record)
value = account and 'account.account,' + str(acc_template_ref[account.id]) or False
if value:
field = self.env['ir.model.fields'].search([('name', '=', record), ('model', '=', 'product.category'), ('relation', '=', 'account.account')], limit=1)
vals = {
'name': record,
'company_id': company.id,
'fields_id': field.id,
'value': value,
}
properties = PropertyObj.search([('name', '=', record), ('company_id', '=', company.id)])
if properties:
#the property exist: modify it
properties.write(vals)
else:
#create the property
PropertyObj.create(vals)
return True
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<odoo>
<data noupdate="1">
<!-- Used in other modules for the tests, NOT in this one -->
@@ -18,4 +18,4 @@
</record>
</data>
</openerp>
</odoo>
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<odoo>
<data>
<record id="view_category_property_form" model="ir.ui.view">
@@ -99,4 +99,4 @@
</field>
</record>
</data>
</openerp>
</odoo>
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<odoo>
<data>
<record id="view_inventory_form_inherit" model="ir.ui.view">
@@ -39,4 +39,4 @@
</record>
</data>
</openerp>
</odoo>
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<odoo>
<data>
<record id="view_stock_config_settings_inherit" model="ir.ui.view">
<field name="name">stock.config.settings.inherit</field>
@@ -8,11 +8,11 @@
<field name="arch" type="xml">
<xpath expr="//group[@name='traceability']" position="after">
<group string="Stock Accounting">
<field name="module_stock_landed_costs" widget="radio" on_change="onchange_landed_costs(module_stock_landed_costs)"/>
<field name="module_stock_landed_costs" widget="radio"/>
<field name="group_stock_inventory_valuation" widget="radio"/>
</group>
</xpath>
</field>
</record>
</data>
</openerp>
</odoo>
+1 -1
View File
@@ -2,4 +2,4 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import stock_change_standard_price
import stock_valuation_history
import wizard_valuation_history
@@ -1,71 +1,42 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp.osv import fields, osv
from openerp.tools.translate import _
import openerp.addons.decimal_precision as dp
from odoo import api, fields, models
from odoo.addons import decimal_precision as dp
class change_standard_price(osv.osv_memory):
class StockChangeStandardPrice(models.TransientModel):
_name = "stock.change.standard.price"
_description = "Change Standard Price"
_columns = {
'new_price': fields.float('Price', required=True, digits_compute=dp.get_precision('Product Price'),
help="If cost price is increased, stock variation account will be debited "
"and stock output account will be credited with the value = (difference of amount * quantity available).\n"
"If cost price is decreased, stock variation account will be creadited and stock input account will be debited."),
'counterpart_account_id': fields.many2one('account.account', string="Counter-Part Account", required=True, domain=[('deprecated', '=', False)]),
}
new_price = fields.Float(
'Price', digits_compute=dp.get_precision('Product Price'), required=True,
help="If cost price is increased, stock variation account will be debited "
"and stock output account will be credited with the value = (difference of amount * quantity available).\n"
"If cost price is decreased, stock variation account will be creadited and stock input account will be debited.")
counterpart_account_id = fields.Many2one(
'account.account', string="Counter-Part Account",
domain=[('deprecated', '=', False)], required=True)
def default_get(self, cr, uid, fields, context=None):
""" To get default values for the object.
@param self: The object pointer.
@param cr: A database cursor
@param uid: ID of the user currently logged in
@param fields: List of fields for which we want default values
@param context: A standard dictionary
@return: A dictionary which of fields with values.
"""
if context is None:
context = {}
if context.get("active_model") == 'product.product':
product_pool = self.pool.get('product.product')
else:
product_pool = self.pool.get('product.template')
product_obj = product_pool.browse(cr, uid, context.get('active_id', False))
@api.model
def default_get(self, fields):
res = super(StockChangeStandardPrice, self).default_get(fields)
res = super(change_standard_price, self).default_get(cr, uid, fields, context=context)
price = context.get('default_new_price') or product_obj.standard_price
if 'new_price' in fields:
res.update({'new_price': price})
if 'counterpart_account_id' in fields:
default_account = product_obj.property_account_expense_id or product_obj.categ_id.property_account_expense_categ_id
if default_account:
res.update({'counterpart_account_id': default_account.id})
product_or_template = self.env[self._context['active_model']].browse(self._context['active_id'])
if 'new_price' in fields and 'default_new_price' not in res:
res['new_price'] = product_or_template.standard_price
if 'counterpart_account_id' in fields and 'counterpart_account_id' not in res:
res['counterpart_account_id'] = product_or_template.property_account_expense_id.id or product_or_template.categ_id.property_account_expense_categ_id.id
return res
def change_price(self, cr, uid, ids, context=None):
""" Changes the Standard Price of Product.
And creates an account move accordingly.
@param self: The object pointer.
@param cr: A database cursor
@param uid: ID of the user currently logged in
@param ids: List of IDs selected
@param context: A standard dictionary
@return:
"""
if context is None:
context = {}
rec_id = context.get('active_id', False)
assert rec_id, _('Active ID is not set in Context.')
new_price = self.browse(cr, uid, ids, context=context)[0].new_price
if context.get("active_model") == 'product.template':
prod_obj = self.pool.get('product.template')
rec_ids = prod_obj.browse(cr, uid, rec_id, context=context).product_variant_ids.mapped('id')
@api.multi
def change_price(self):
""" Changes the Standard Price of Product and creates an account move accordingly. """
self.ensure_one()
if self._context['active_model'] == 'product.template':
products = self.env['product.template'].browse(self._context['active_id']).product_variant_ids
else:
rec_ids = [rec_id]
res = self.browse(cr, uid, ids, context=context)[0]
self.pool.get('product.product').do_change_standard_price(cr, uid, rec_ids, new_price, res.counterpart_account_id.id, context=context)
products = self.env['product.product'].browse(self._context['active_id'])
products.do_change_standard_price(self.new_price, self.counterpart_account_id.id)
return {'type': 'ir.actions.act_window_close'}
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<odoo>
<data>
<record id="view_change_standard_price" model="ir.ui.view">
<field name="name">Change Standard Price</field>
@@ -34,5 +34,4 @@
</record>
</data>
</openerp>
</odoo>
@@ -1,207 +0,0 @@
from datetime import datetime
from openerp import tools
from openerp.osv import fields, osv
from openerp.tools.translate import _
class wizard_valuation_history(osv.osv_memory):
_name = 'wizard.valuation.history'
_description = 'Wizard that opens the stock valuation history table'
_columns = {
'choose_date': fields.boolean('Inventory at Date'),
'date': fields.datetime('Date', required=True),
}
_defaults = {
'choose_date': False,
'date': fields.datetime.now,
}
def open_table(self, cr, uid, ids, context=None):
if context is None:
context = {}
data = self.read(cr, uid, ids, context=context)[0]
ctx = context.copy()
ctx['history_date'] = data['date']
ctx['search_default_group_by_product'] = True
ctx['search_default_group_by_location'] = True
return {
'domain': "[('date', '<=', '" + data['date'] + "')]",
'name': _('Stock Value At Date'),
'view_type': 'form',
'view_mode': 'tree',
'res_model': 'stock.history',
'type': 'ir.actions.act_window',
'context': ctx,
}
class stock_history(osv.osv):
_name = 'stock.history'
_auto = False
_order = 'date asc'
def read_group(self, cr, uid, domain, fields, groupby, offset=0, limit=None, context=None, orderby=False, lazy=True):
res = super(stock_history, self).read_group(cr, uid, domain, fields, groupby, offset=offset, limit=limit, context=context, orderby=orderby, lazy=lazy)
if context is None:
context = {}
date = context.get('history_date', datetime.now())
if 'inventory_value' in fields:
group_lines = {}
for line in res:
domain = line.get('__domain', domain)
group_lines.setdefault(str(domain), self.search(cr, uid, domain, context=context))
line_ids = set()
for ids in group_lines.values():
for product_id in ids:
line_ids.add(product_id)
line_ids = list(line_ids)
lines_rec = {}
if line_ids:
cr.execute('SELECT id, product_id, price_unit_on_quant, company_id, quantity FROM stock_history WHERE id in %s', (tuple(line_ids),))
lines_rec = cr.dictfetchall()
lines_dict = dict((line['id'], line) for line in lines_rec)
product_ids = list(set(line_rec['product_id'] for line_rec in lines_rec))
products_rec = self.pool['product.product'].read(cr, uid, product_ids, ['cost_method', 'id'], context=context)
products_dict = dict((product['id'], product) for product in products_rec)
cost_method_product_ids = list(set(product['id'] for product in products_rec if product['cost_method'] != 'real'))
histories = []
if cost_method_product_ids:
cr.execute('SELECT DISTINCT ON (product_id, company_id) product_id, company_id, cost FROM product_price_history WHERE product_id in %s AND datetime <= %s ORDER BY product_id, company_id, datetime DESC', (tuple(cost_method_product_ids), date))
histories = cr.dictfetchall()
histories_dict = {}
for history in histories:
histories_dict[(history['product_id'], history['company_id'])] = history['cost']
for line in res:
inv_value = 0.0
lines = group_lines.get(str(line.get('__domain', domain)))
for line_id in lines:
line_rec = lines_dict[line_id]
product = products_dict[line_rec['product_id']]
if product['cost_method'] == 'real':
price = line_rec['price_unit_on_quant']
else:
price = histories_dict.get((product['id'], line_rec['company_id']), 0.0)
inv_value += price * line_rec['quantity']
line['inventory_value'] = inv_value
return res
def _get_inventory_value(self, cr, uid, ids, name, attr, context=None):
if context is None:
context = {}
date = context.get('history_date')
product_obj = self.pool.get("product.product")
res = {}
for line in self.browse(cr, uid, ids, context=context):
if line.product_id.cost_method == 'real':
res[line.id] = line.quantity * line.price_unit_on_quant
else:
res[line.id] = line.quantity * product_obj.get_history_price(cr, uid, line.product_id.id, line.company_id.id, date=date, context=context)
return res
_columns = {
'move_id': fields.many2one('stock.move', 'Stock Move', required=True),
'location_id': fields.many2one('stock.location', 'Location', required=True),
'company_id': fields.many2one('res.company', 'Company'),
'product_id': fields.many2one('product.product', 'Product', required=True),
'product_categ_id': fields.many2one('product.category', 'Product Category', required=True),
'quantity': fields.float('Product Quantity'),
'date': fields.datetime('Operation Date'),
'price_unit_on_quant': fields.float('Value'),
'inventory_value': fields.function(_get_inventory_value, string="Inventory Value", type='float', readonly=True),
'source': fields.char('Source'),
'product_template_id': fields.many2one('product.template', 'Product Template', required=True),
'serial_number': fields.char('Lot/Serial Number', required=True),
}
def init(self, cr):
tools.drop_view_if_exists(cr, 'stock_history')
cr.execute("""
CREATE OR REPLACE VIEW stock_history AS (
SELECT MIN(id) as id,
move_id,
location_id,
company_id,
product_id,
product_categ_id,
product_template_id,
SUM(quantity) as quantity,
date,
SUM(price_unit_on_quant * quantity) / SUM(quantity) as price_unit_on_quant,
source,
string_agg(DISTINCT serial_number, ', ' ORDER BY serial_number) AS serial_number
FROM
((SELECT
stock_move.id AS id,
stock_move.id AS move_id,
dest_location.id AS location_id,
dest_location.company_id AS company_id,
stock_move.product_id AS product_id,
product_template.id AS product_template_id,
product_template.categ_id AS product_categ_id,
quant.qty AS quantity,
stock_move.date AS date,
quant.cost as price_unit_on_quant,
stock_move.origin AS source,
stock_production_lot.name AS serial_number
FROM
stock_quant as quant
JOIN
stock_quant_move_rel ON stock_quant_move_rel.quant_id = quant.id
JOIN
stock_move ON stock_move.id = stock_quant_move_rel.move_id
LEFT JOIN
stock_production_lot ON stock_production_lot.id = quant.lot_id
JOIN
stock_location dest_location ON stock_move.location_dest_id = dest_location.id
JOIN
stock_location source_location ON stock_move.location_id = source_location.id
JOIN
product_product ON product_product.id = stock_move.product_id
JOIN
product_template ON product_template.id = product_product.product_tmpl_id
WHERE quant.qty>0 AND stock_move.state = 'done' AND dest_location.usage in ('internal', 'transit')
AND (
not (source_location.company_id is null and dest_location.company_id is null) or
source_location.company_id != dest_location.company_id or
source_location.usage not in ('internal', 'transit'))
) UNION ALL
(SELECT
(-1) * stock_move.id AS id,
stock_move.id AS move_id,
source_location.id AS location_id,
source_location.company_id AS company_id,
stock_move.product_id AS product_id,
product_template.id AS product_template_id,
product_template.categ_id AS product_categ_id,
- quant.qty AS quantity,
stock_move.date AS date,
quant.cost as price_unit_on_quant,
stock_move.origin AS source,
stock_production_lot.name AS serial_number
FROM
stock_quant as quant
JOIN
stock_quant_move_rel ON stock_quant_move_rel.quant_id = quant.id
JOIN
stock_move ON stock_move.id = stock_quant_move_rel.move_id
LEFT JOIN
stock_production_lot ON stock_production_lot.id = quant.lot_id
JOIN
stock_location source_location ON stock_move.location_id = source_location.id
JOIN
stock_location dest_location ON stock_move.location_dest_id = dest_location.id
JOIN
product_product ON product_product.id = stock_move.product_id
JOIN
product_template ON product_template.id = product_product.product_tmpl_id
WHERE quant.qty>0 AND stock_move.state = 'done' AND source_location.usage in ('internal', 'transit')
AND (
not (dest_location.company_id is null and source_location.company_id is null) or
dest_location.company_id != source_location.company_id or
dest_location.usage not in ('internal', 'transit'))
))
AS foo
GROUP BY move_id, location_id, company_id, product_id, product_categ_id, date, source, product_template_id
)""")
@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, models, fields, _
class WizardValuationHistory(models.TransientModel):
_name = 'wizard.valuation.history'
_description = 'Wizard that opens the stock valuation history table'
choose_date = fields.Boolean('Inventory at Date')
date = fields.Datetime('Date', default=fields.Datetime.now, required=True)
@api.multi
def open_table(self):
self.ensure_one()
ctx = dict(
self._context,
history_date=self.date,
search_default_group_by_product=True,
search_default_group_by_location=True)
return {
'domain': "[('date', '<=', '" + self.date + "')]",
'name': _('Stock Value At Date'),
'view_type': 'form',
'view_mode': 'tree',
'res_model': 'stock.history',
'type': 'ir.actions.act_window',
'context': ctx,
}
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_wizard_valuation_history" model="ir.ui.view">
<field name="name">Inventory at Date</field>
<field name="model">wizard.valuation.history</field>
<field name="arch" type="xml">
<form string="Choose your date">
<p class="oe_gray" attrs="{'invisible': [('choose_date', '=', False)]}">
Choose a date in the past to get the inventory at that date.
</p>
<p class="oe_gray" attrs="{'invisible': [('choose_date', '=', True)]}">
Retrieve the curent stock valuation.
</p>
<group>
<field name="choose_date"/>
<field name="date" attrs="{'invisible': [('choose_date', '=', False)]}"/>
</group>
<footer>
<button name="open_table" string="Retrieve the Inventory Value" type="object" class="btn-primary"/>
<button string="Cancel" class="btn-default" special="cancel" />
</footer>
</form>
</field>
</record>
<record id="action_wizard_stock_valuation_history" model="ir.actions.act_window">
<field name="name">Inventory at Date</field>
<field name="res_model">wizard.valuation.history</field>
<field name="view_type">form</field>
<field name="view_mode">form</field>
<field name="view_id" ref="view_wizard_valuation_history"/>
<field name="target">new</field>
</record>
<menuitem id="menu_action_wizard_valuation_history" action="action_wizard_stock_valuation_history" parent="stock.menu_warehouse_report" sequence="1" groups="base.group_no_one"/>
</odoo>
+1 -3
View File
@@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import stock
import procurement
import resource
import models
+3 -2
View File
@@ -45,10 +45,11 @@
],
'data': [
'stock_calendar_view.xml'
'views/stock_views.xml',
'views/procurement_views.xml',
'views/resource_views.xml',
],
'test': [
'test/orderpoint_calendar.yml'
],
'installable': True,
'application': False,
+6
View File
@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import procurement
import resource
import stock
@@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from odoo import api, fields, models
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
class ProcurementOrder(models.Model):
_inherit = 'procurement.order'
next_delivery_date = fields.Datetime('Next Delivery Date', help="The date of the next delivery for this procurement group, when this group is on the purchase calendar of the orderpoint")
next_purchase_date = fields.Datetime('Next Purchase Date', help="The date the next purchase order should be sent to the vendor")
@api.multi
def assign_group_date(self):
Orderpoint = self.env["stock.warehouse.orderpoint"]
for procurement in self:
orderpoint = Orderpoint.search([
('location_id', '=', procurement.location_id.id),
('product_id', '=', procurement.product_id.id)], limit=1)
if orderpoint:
date_planned = fields.Datetime.from_string(procurement.date_planned)
purchase_date, delivery_date = orderpoint._get_previous_dates(date_planned)
if purchase_date and delivery_date:
procurement.write({
'next_delivery_date': delivery_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
'next_purchase_date': purchase_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT)})
def _get_purchase_order_date(self, schedule_date):
if self.next_purchase_date:
return fields.Datetime.from_string(self.next_purchase_date)
return super(ProcurementOrder, self)._get_purchase_order_date(schedule_date)
def _get_purchase_schedule_date(self):
if self.next_delivery_date:
return datetime.strptime(self.next_delivery_date, DEFAULT_SERVER_DATETIME_FORMAT)
return super(ProcurementOrder, self)._get_purchase_schedule_date()
def _prepare_purchase_order_line(self, po, supplier):
res = super(ProcurementOrder, self)._prepare_purchase_order_line(po, supplier)
if self.next_delivery_date:
res.update({'date_planned': self.next_delivery_date})
return res
def _procurement_from_orderpoint_get_order(self):
return 'location_id, purchase_calendar_id, calendar_id'
def _procurement_from_orderpoint_get_grouping_key(self, orderpoint_ids):
orderpoint = self.env['stock.warehouse.orderpoint'].browse(orderpoint_ids[0])
return (orderpoint.location_id.id, orderpoint.purchase_calendar_id.id, orderpoint.calendar_id.id)
def _procurement_from_orderpoint_get_groups(self, orderpoint_ids):
orderpoint = self.env['stock.warehouse.orderpoint'].browse(orderpoint_ids[0])
res_groups = []
date_groups = orderpoint._get_group()
for date, group in date_groups:
if orderpoint.calendar_id and orderpoint.calendar_id.attendance_ids:
date1, date2 = orderpoint._get_next_dates(date, group)
res_groups += [{'to_date': date2, 'procurement_values': {'group': group, 'date': date1, 'purchase_date': date}}] # date1/date2 as deliveries and date as purchase confirmation date
else:
res_groups += [{'to_date': False, 'procurement_values': {'group': group, 'date': date, 'purchase_date': date}}]
return res_groups
def _procurement_from_orderpoint_post_process(self, orderpoint_ids):
self.env['stock.warehouse.orderpoint'].browse(orderpoint_ids).write({
'last_execution_date': datetime.utcnow().strftime(DEFAULT_SERVER_DATETIME_FORMAT)})
return super(ProcurementOrder, self)._procurement_from_orderpoint_post_process(orderpoint_ids)
@@ -1,33 +1,27 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import datetime
from openerp.osv import fields, osv
from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT, DEFAULT_SERVER_DATE_FORMAT
from odoo import api, fields, models
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT, DEFAULT_SERVER_DATE_FORMAT
class resource_calendar_leaves(osv.osv):
class ResourceCalendarAttendance(models.Model):
_inherit = "resource.calendar.attendance"
group_id = fields.Many2one('procurement.group', 'Procurement Group')
class ResourceCalendarLeaves(models.Model):
_inherit = "resource.calendar.leaves"
_columns = {
'group_id': fields.many2one('procurement.group', string="Procurement Group"),
}
class resource_calendar(osv.osv):
group_id = fields.Many2one('procurement.group', 'Procurement Group')
class ResourceCalendar(models.Model):
_inherit = "resource.calendar"
#Could remove this as it does not help a lot
def _calculate_next_day(self, cr, uid, ids, fields, names, context=None):
res = {}
for calend in self.browse(cr, uid, ids, context=context):
# date1 = self.get_next_day(cr, uid, calend.id, datetime.utcnow() + relativedelta(days = 1))
_format = '%Y-%m-%d %H:%M:%S'
sched_date = self.schedule_days_get_date(
cr, uid, calend.id, 1, day_date=datetime.datetime.utcnow(), compute_leaves=True)
res[calend.id] = sched_date and sched_date.strftime(_format) or False
return res
_columns = {
'next_day': fields.function(_calculate_next_day, string='Next day it should trigger', type='datetime'),
}
# Keep as it takes into account times
def get_leave_intervals(self, cr, uid, id, resource_id=None,
start_datetime=None, end_datetime=None,
@@ -47,6 +41,8 @@ class resource_calendar(osv.osv):
:return list leaves: list of tuples (start_datetime, end_datetime) of
leave intervals
"""
# TDE FIXME: waw, overriding completely a method + changing its result
# and its behavior, very nice. Please FIXME.
resource_calendar = self.browse(cr, uid, id, context=context)
leaves = []
for leave in resource_calendar.leave_ids:
@@ -90,6 +86,8 @@ class resource_calendar(osv.osv):
that are intervals to remove from the base interval
:return list intervals: a list of tuples (begin datetime, end datetime)
that are the remaining valid intervals """
# TDE FIXME: waw, overriding completely a method + changing its result
# and its behavior, very nice. Please FIXME.
if not interval:
return interval
if leave_intervals is None:
@@ -124,11 +122,3 @@ class resource_calendar(osv.osv):
else:
intervals.append((current_interval[0], current_interval[1],))
return intervals
class resource_calendar_attendance(osv.osv):
_inherit = "resource.calendar.attendance"
_columns = {
'group_id': fields.many2one('procurement.group', 'Procurement Group'),
}
+134
View File
@@ -0,0 +1,134 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime
from dateutil.relativedelta import relativedelta
from odoo import fields, models
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
class StockWarehouseOrderpoint(models.Model):
_inherit = "stock.warehouse.orderpoint"
calendar_id = fields.Many2one(
'resource.calendar', 'Calendar',
help="In the calendar you can define the days that the goods will be delivered. That way the scheduler will "
"only take into account the goods needed until the second delivery and put the procurement date as the first delivery.")
purchase_calendar_id = fields.Many2one('resource.calendar', 'Purchase Calendar')
last_execution_date = fields.Datetime('Last Execution Date', readonly=True)
def _prepare_procurement_values(self, product_qty, date=False, purchase_date=False, group=False):
res = super(StockWarehouseOrderpoint, self)._prepare_procurement_values(product_qty, date=date, group=group)
res.update({
'next_delivery_date': date,
'next_purchase_date': purchase_date})
return res
def _get_group(self):
"""
Will return the groups and the end dates of the intervals of the purchase calendar
that need to be executed now.
If a purchase calendar is defined, it should give the
:return [(date, group)]
"""
# TDE FIXME: unused context key 'no_round_hours' removed
# Check if orderpoint has last execution date and calculate if we need to calculate again already
Calendar = self.env["resource.calendar"]
Attendance = self.env["resource.calendar.attendance"]
group = False
date = False
now_date = datetime.utcnow()
res_intervals = []
if self.purchase_calendar_id and self.purchase_calendar_id.attendance_ids:
if self.last_execution_date:
new_date = datetime.strptime(self.last_execution_date, DEFAULT_SERVER_DATETIME_FORMAT)
else:
new_date = datetime.utcnow()
# TDE note: I bet accessing interval[2] will crash, no ? this code seems very louche
intervals = Calendar._schedule_days(self.purchase_calendar_id.id, 1, new_date, compute_leaves=True)
for interval in intervals:
# If last execution date, interval should start after it in order not to execute the same orderpoint twice
# TODO: Make the interval a little bigger
if (self.last_execution_date and (interval[0] > new_date and interval[0] < now_date)) or (not self.last_execution_date and interval[0] < now_date and interval[1] > now_date):
group = Attendance.browse(interval[2]).group_id.id
date = interval[1]
res_intervals += [(date, group), ]
else:
return [(now_date, None)]
return res_intervals
def _get_previous_dates(self, start_date=False):
"""
Date should be given in utc
"""
# TDE FIXME: unused context key 'no_round_hours' removed
Attendance = self.env["resource.calendar.attendance"]
# First check if the orderpoint has a Calendar as it should be delivered at this calendar date
purchase_date = False
delivery_date = start_date
if self.calendar_id and self.calendar_id.attendance_ids:
res = self.calendar_id._schedule_days(-1, start_date, compute_leaves=True)[0] # CLEANME
if res and res[0][0] < start_date:
group_to_find = res[0][2] and Attendance.browse(res[0][2]).group_id.id
delivery_date = res[0][0]
found_date = delivery_date
if self.purchase_calendar_id and self.purchase_calendar_id.attendance_ids:
while not purchase_date:
found_date = found_date + relativedelta(days=-1) # won't allow to deliver within the day
res = self.purchase_calendar_id._schedule_days(-1, found_date, compute_leaves=True)[0] # CLEANME
for re in res:
group = re[2] and Attendance.browse(re[2]).group_id.id
found_date = re[0]
if not purchase_date and (group_to_find and group_to_find == group or (not group_to_find)):
purchase_date = re[0]
else:
delivery_date = start_date or datetime.utcnow()
return purchase_date, delivery_date
def _get_next_dates(self, new_date=False, group=False):
# TDE FIXME: unused context key 'no_round_hours' removed
Attendance = self.env["resource.calendar.attendance"]
if not new_date:
new_date = datetime.utcnow()
now_date = datetime.utcnow()
# Search first calendar day (without group)
res = self.calendar_id._schedule_days(1, new_date, compute_leaves=True)[0] # CLEANME
att_group = res and res[0][2] and Attendance.browse(res[0][2]).group_id.id
# If hours are smaller than the current date, search a day further
if res and res[0][0] < now_date:
new_date = res[0][1] + relativedelta(days=1)
res = self.calendar_id._schedule_days(1, new_date, compute_leaves=True)
for re in res:
att_group = False
if re[2]:
att_group = Attendance.browse(re[2]).group_id.id
if att_group == group:
break
# If you can find an entry and you have a group to match, but it does not, search further until you find one that corresponds
number = 0
while res and group and att_group != group and number < 100:
number += 1
new_date = res[0][1] + relativedelta(days=1)
res = self.calendar_id._schedule_days(1, new_date, compute_leaves=True)[0]
att_group = False
for re in res:
if re[2]:
att_group = Attendance.browse(re[2]).group_id.id
if att_group == group:
break
# number as safety pall for endless loops
if number >= 100:
res = False
# If you found a solution(first date), you need the second date until the next delivery because you need to deliver
# everything needed until the second date on the first date
if res:
date1 = res[0][1]
new_date = res[0][1] + relativedelta(days=1)
res = self.calendar_id._schedule_days(1, new_date, compute_leaves=True)[0]
if res:
return (date1, res[0][1])
return (False, False)
-220
View File
@@ -1,220 +0,0 @@
from datetime import datetime
from dateutil.relativedelta import relativedelta
from openerp import api
from openerp.osv import fields, osv
from openerp.tools.translate import _
from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT, DEFAULT_SERVER_DATE_FORMAT
from openerp import SUPERUSER_ID
from psycopg2 import OperationalError
from openerp.tools import float_compare, float_round
import pytz
import openerp
class procurement_order(osv.osv):
_inherit = 'procurement.order'
_columns = {
'next_delivery_date': fields.datetime('Next Delivery Date',
help="The date of the next delivery for this procurement group, when this group is on the purchase calendar of the orderpoint"),
'next_purchase_date': fields.datetime('Next Purchase Date',
help="The date the next purchase order should be sent to the vendor"),
}
def assign_group_date(self, cr, uid, ids, context=None):
orderpoint_obj = self.pool.get("stock.warehouse.orderpoint")
for procurement in self.browse(cr, uid, ids, context=context):
ops = orderpoint_obj.search(cr, uid, [('location_id', '=', procurement.location_id.id),
('product_id', '=', procurement.product_id.id)], context=context)
if ops and ops[0]:
orderpoint = orderpoint_obj.browse(cr, uid, ops[0], context=context)
date_planned = datetime.strptime(procurement.date_planned, DEFAULT_SERVER_DATETIME_FORMAT)
purchase_date, delivery_date = self._get_previous_dates(cr, uid, orderpoint, date_planned, context=context)
if purchase_date and delivery_date:
self.write(cr, uid, [procurement.id], {'next_delivery_date': delivery_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
'next_purchase_date': purchase_date.strftime(DEFAULT_SERVER_DATETIME_FORMAT),}, context=context)
@api.v8
def _get_purchase_order_date(self, schedule_date):
if self.next_purchase_date:
return datetime.strptime(self.next_purchase_date, DEFAULT_SERVER_DATETIME_FORMAT)
return super(procurement_order, self)._get_purchase_order_date(schedule_date)
@api.v7
def _get_purchase_order_date(self, cr, uid, procurement, company, schedule_date, context=None):
"""Return the datetime value to use as Order Date (``date_order``) for the
Purchase Order created to satisfy the given procurement.
:param browse_record procurement: the procurement for which a PO will be created.
:param browse_report company: the company to which the new PO will belong to.
:param datetime schedule_date: desired Scheduled Date for the Purchase Order lines.
:rtype: datetime
:return: the desired Order Date for the PO
"""
if procurement.next_purchase_date:
return datetime.strptime(procurement.next_purchase_date, DEFAULT_SERVER_DATETIME_FORMAT)
return super(procurement_order, self)._get_purchase_order_date(cr, uid, procurement, company, schedule_date, context=context)
@api.v8
def _get_purchase_schedule_date(self):
if self.next_delivery_date:
return datetime.strptime(self.next_delivery_date, DEFAULT_SERVER_DATETIME_FORMAT)
return super(procurement_order, self)._get_purchase_schedule_date()
@api.v7
def _get_purchase_schedule_date(self, cr, uid, procurement, context=None):
"""Return the datetime value to use as Schedule Date (``date_planned``) for the
Purchase Order Lines created to satisfy the given procurement.
:param browse_record procurement: the procurement for which a PO will be created.
:rtype: datetime
:return: the desired Schedule Date for the PO lines
"""
if procurement.next_delivery_date:
return datetime.strptime(procurement.next_delivery_date, DEFAULT_SERVER_DATETIME_FORMAT)
return super(procurement_order, self)._get_purchase_schedule_date(cr, uid, procurement, context=context)
def _prepare_purchase_order_line(self, cr, uid, ids, po, supplier, context=None):
res = super(procurement_order, self)._prepare_purchase_order_line(cr, uid, ids, po, supplier, context=context)
procurement = self.browse(cr, uid, ids, context=context)
if procurement.next_delivery_date:
res.update({'date_planned': procurement.next_delivery_date})
return res
def _get_previous_dates(self, cr, uid, orderpoint, start_date = False, context=None):
"""
Date should be given in utc
"""
calendar_obj = self.pool.get('resource.calendar')
att_obj = self.pool.get('resource.calendar.attendance')
context = context or {}
context['no_round_hours'] = True
# First check if the orderpoint has a Calendar as it should be delivered at this calendar date
purchase_date = False
delivery_date = start_date
if orderpoint.calendar_id and orderpoint.calendar_id.attendance_ids:
res = calendar_obj._schedule_days(cr, uid, orderpoint.calendar_id.id, -1, start_date, compute_leaves=True, context=context)
if res and res[0][0] < start_date:
group_to_find = res[0][2] and att_obj.browse(cr, uid, res[0][2], context=context).group_id.id or False
delivery_date = res[0][0]
found_date = delivery_date
if orderpoint.purchase_calendar_id and orderpoint.purchase_calendar_id.attendance_ids:
while not purchase_date:
found_date = found_date + relativedelta(days=-1) # won't allow to deliver within the day
res = calendar_obj._schedule_days(cr, uid, orderpoint.purchase_calendar_id.id, -1, found_date, compute_leaves=True, context=context)
for re in res:
group = re[2] and att_obj.browse(cr, uid, re[2], context=context).group_id.id or False
found_date = re[0]
if not purchase_date and (group_to_find and group_to_find == group or (not group_to_find)):
purchase_date = re[0]
else:
delivery_date = start_date or datetime.utcnow()
return purchase_date, delivery_date
def _get_next_dates(self, cr, uid, orderpoint, new_date=False, group=False, context=None):
calendar_obj = self.pool.get('resource.calendar')
att_obj = self.pool.get('resource.calendar.attendance')
context = context or {}
context['no_round_hours'] = True
if not new_date:
new_date = datetime.utcnow()
now_date = datetime.utcnow()
# Search first calendar day (without group)
res = calendar_obj._schedule_days(cr, uid, orderpoint.calendar_id.id, 1, new_date, compute_leaves=True, context=context)
att_group = res and res[0][2] and att_obj.browse(cr, uid, res[0][2], context=context).group_id.id or False
#If hours are smaller than the current date, search a day further
if res and res[0][0] < now_date:
new_date = res[0][1] + relativedelta(days=1)
res = calendar_obj._schedule_days(cr, uid, orderpoint.calendar_id.id, 1, new_date, compute_leaves=True, context=context)
for re in res:
att_group = False
if re[2]:
att_group = att_obj.browse(cr, uid, re[2], context=context).group_id.id
if att_group == group:
break
# If you can find an entry and you have a group to match, but it does not, search further until you find one that corresponds
number = 0
while res and group and att_group != group and number < 100:
number += 1
new_date = res[0][1] + relativedelta(days=1)
res = calendar_obj._schedule_days(cr, uid, orderpoint.calendar_id.id, 1, new_date, compute_leaves=True, context=context)
att_group = False
for re in res:
if re[2]:
att_group = att_obj.browse(cr, uid, re[2], context=context).group_id.id
if att_group == group:
break
#number as safety pall for endless loops
if number >= 100:
res = False
# If you found a solution(first date), you need the second date until the next delivery because you need to deliver
# everything needed until the second date on the first date
if res:
date1 = res[0][1]
new_date = res[0][1] + relativedelta(days=1)
res = calendar_obj._schedule_days(cr, uid, orderpoint.calendar_id.id, 1, new_date, compute_leaves=True, context=context)
if res:
return (date1, res[0][1])
return (False, False)
def _get_group(self, cr, uid, orderpoint, context=None):
"""
Will return the groups and the end dates of the intervals of the purchase calendar
that need to be executed now.
If a purchase calendar is defined, it should give the
:return [(date, group)]
"""
#Check if orderpoint has last execution date and calculate if we need to calculate again already
calendar_obj = self.pool.get("resource.calendar")
att_obj = self.pool.get("resource.calendar.attendance")
group = False
context = context or {}
context['no_round_hours'] = True
date = False
now_date = datetime.utcnow()
res_intervals = []
if orderpoint.purchase_calendar_id and orderpoint.purchase_calendar_id.attendance_ids:
if orderpoint.last_execution_date:
new_date = datetime.strptime(orderpoint.last_execution_date, DEFAULT_SERVER_DATETIME_FORMAT)
else:
new_date = datetime.utcnow()
# TDE note: I bet accessing interval[2] will crash, no ? this code seems very louche
intervals = calendar_obj._schedule_days(cr, uid, orderpoint.purchase_calendar_id.id, 1, new_date, compute_leaves=True, context=context)
for interval in intervals:
# If last execution date, interval should start after it in order not to execute the same orderpoint twice
# TODO: Make the interval a little bigger
if (orderpoint.last_execution_date and (interval[0] > new_date and interval[0] < now_date)) or (not orderpoint.last_execution_date and interval[0] < now_date and interval[1] > now_date):
group = att_obj.browse(cr, uid, interval[2], context=context).group_id.id
date = interval[1]
res_intervals += [(date, group), ]
else:
return [(now_date, None)]
return res_intervals
def _procurement_from_orderpoint_get_order(self, cr, uid, context=None):
return 'location_id, purchase_calendar_id, calendar_id'
def _procurement_from_orderpoint_get_grouping_key(self, cr, uid, orderpoint_ids, context=None):
orderpoint = self.pool['stock.warehouse.orderpoint'].browse(cr, uid, orderpoint_ids[0], context=context)
return (orderpoint.location_id.id, orderpoint.purchase_calendar_id.id, orderpoint.calendar_id.id)
def _procurement_from_orderpoint_get_groups(self, cr, uid, orderpoint_ids, context=None):
orderpoint = self.pool['stock.warehouse.orderpoint'].browse(cr, uid, orderpoint_ids[0], context=context)
res_groups = []
date_groups = self._get_group(cr, uid, orderpoint, context=context)
for date, group in date_groups:
if orderpoint.calendar_id and orderpoint.calendar_id.attendance_ids:
date1, date2 = self._get_next_dates(cr, uid, orderpoint, date, group, context=context)
res_groups += [{'to_date': date2, 'procurement_values': {'group': group, 'date': date1, 'purchase_date': date}}] # date1/date2 as deliveries and date as purchase confirmation date
else:
res_groups += [{'to_date': False, 'procurement_values': {'group': group, 'date': date, 'purchase_date': date}}]
return res_groups
def _procurement_from_orderpoint_post_process(self, cr, uid, orderpoint_ids, context=None):
self.pool['stock.warehouse.orderpoint'].write(
cr, uid, orderpoint_ids, {
'last_execution_date': datetime.utcnow().strftime(DEFAULT_SERVER_DATETIME_FORMAT)
}, context=context)
return super(procurement_order, self)._procurement_from_orderpoint_post_process(cr, uid, orderpoint_ids, context=context)
-18
View File
@@ -1,18 +0,0 @@
from openerp.osv import fields, osv
class stock_warehouse_orderpoint(osv.osv):
_inherit = "stock.warehouse.orderpoint"
_columns = {
'calendar_id': fields.many2one('resource.calendar', 'Calendar',
help="In the calendar you can define the days that the goods will be delivered. That way the scheduler will only take into account the goods needed until the second delivery and put the procurement date as the first delivery. "),
'purchase_calendar_id': fields.many2one('resource.calendar', 'Purchase Calendar'),
'last_execution_date': fields.datetime('Last Execution Date', readonly=True),
}
def _prepare_procurement_values(self, cr, uid, ids, product_qty, date=False, purchase_date=False, group=False, context=None):
res = super(stock_warehouse_orderpoint, self)._prepare_procurement_values(cr, uid, ids, product_qty, date=date, group=group, context=context)
res.update({
'next_delivery_date': date,
'next_purchase_date': purchase_date})
return res
@@ -1,40 +0,0 @@
<openerp>
<data>
<record id="view_warehouse_orderpoint_form_inherit" model="ir.ui.view">
<field name="name">stock.warehouse.orderpoint.form.inherit</field>
<field name="model">stock.warehouse.orderpoint</field>
<field name="inherit_id" ref="stock.view_warehouse_orderpoint_form"/>
<field name="arch" type="xml">
<field name="company_id" position="after">
<field name="calendar_id"/>
<field name="purchase_calendar_id"/>
</field>
</field>
</record>
<!-- Procurement group to be used on the calendar attendance to match triggers and purchases-->
<record id="view_resource_calendar_attendance_tree_procurement_group" model="ir.ui.view">
<field name="name">resource.calendar.attendance.tree.procurement</field>
<field name="inherit_id" ref="resource.view_resource_calendar_attendance_tree"/>
<field name="model">resource.calendar.attendance</field>
<field name="arch" type="xml">
<field name="date_to" position="after">
<field name="group_id"/>
</field>
</field>
</record>
<!-- For testing purposes -->
<record id="view_procurement_form_stock_inherit" model="ir.ui.view">
<field name="name">procurement.order.form.stock.inherit</field>
<field name="model">procurement.order</field>
<field name="inherit_id" ref="procurement.procurement_form_view"/>
<field name="arch" type="xml">
<field name="state" position="before">
<button name="assign_group_date" string="Calculate dates according to Orderpoint" type="object"/>
</field>
</field>
</record>
</data>
</openerp>
@@ -1,98 +0,0 @@
-
Create a partner
-
!record {model: res.partner, id: res_partner_cal}:
name: Supplier
supplier: 1
-
Create a calendar
-
!record {model: resource.calendar, id: res_cal}:
name: Calendar
attendance_ids:
- name: Thursday
dayofweek: '3'
hour_from: 08
hour_to: 09
-
Create a product A with orderpoint with a calendar
-
!record {model: product.product, id: product_calendar, view: False}:
name: Calendar Product
seller_ids:
- name: res_partner_cal
delay: 1
orderpoint_ids:
- name: Product A Truck
calendar_id: res_cal
product_min_qty: 0
product_max_qty: 10
warehouse_id: stock.warehouse0
-
Create delivery order with product A
-
!record {model: stock.picking, id: pick_out_calendar}:
name: Delivery order for procurement (calendar)
partner_id: res_partner_cal
picking_type_id: stock.picking_type_out
move_lines:
- product_id: product_calendar
product_uom_qty: 3.00
location_id: stock.stock_location_stock
location_dest_id: stock.stock_location_customers
procure_method: make_to_stock
-
Create other delivery order with product A that we will give a later date afterwards (date between the two)
-
!record {model: stock.picking, id: pick_out_calendar2}:
name: Delivery order for procurement (calendar2)
partner_id: res_partner_cal
picking_type_id: stock.picking_type_out
move_lines:
- product_id: product_calendar
product_uom_qty: 4.00
location_id: stock.stock_location_stock
location_dest_id: stock.stock_location_customers
procure_method: make_to_stock
-
Create other delivery order with product A that we will give an even later date, so it should not be taken into account
-
!record {model: stock.picking, id: pick_out_calendar3}:
name: Delivery order for procurement (calendar3)
partner_id: res_partner_cal
picking_type_id: stock.picking_type_out
move_lines:
- product_id: product_calendar
product_uom_qty: 11.00
location_id: stock.stock_location_stock
location_dest_id: stock.stock_location_customers
procure_method: make_to_stock
-
Put different dates in pickings, confirm them, run schedulers and check that the procurement generated by the orderpoint is taking the calendar into account. Make sure also that the purchase order creates a datetime.
-
!python {model: stock.picking}: |
move_obj = self.pool.get('stock.move')
product_obj = self.pool.get('product.product')
product_obj.write(cr, uid, [ref('product_calendar')], {'route_ids': [(4, ref('purchase.route_warehouse0_buy'))]})
import datetime
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT
today8 = datetime.datetime.today() + datetime.timedelta(days = 7)
today21 = datetime.datetime.today() + datetime.timedelta(days = 21)
cal1 = self.browse(cr, uid, ref('pick_out_calendar'))
cal2 = self.browse(cr, uid, ref('pick_out_calendar2'))
move_obj.write(cr, uid, cal2.move_lines[0].id, {'date': today8.strftime(DEFAULT_SERVER_DATETIME_FORMAT), 'date_expected': today8.strftime(DEFAULT_SERVER_DATETIME_FORMAT)})
cal3 = self.browse(cr, uid, ref('pick_out_calendar3'))
move_obj.write(cr, uid, cal3.move_lines[0].id, {'date_expected': today21.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
'date': today21.strftime(DEFAULT_SERVER_DATETIME_FORMAT)})
#We need to confirm pickings and run the schedulers
self.action_confirm(cr, uid, [cal1.id, cal2.id, cal3.id])
# Check procurements generated
proc_obj = self.pool.get("procurement.order")
proc_obj.run_scheduler(cr, uid)
proc_ids = proc_obj.search(cr, uid, [('product_id', '=', ref('product_calendar'))])
assert len(proc_ids) == 1, 'should have one procurement'
procurement = proc_obj.browse(cr, uid, proc_ids[0])
assert procurement.product_qty == 17, 'It should have taken the two first pickings into account for the virtual stock for the orderpoint, not the third'
assert datetime.datetime.strptime(procurement.next_delivery_date, DEFAULT_SERVER_DATETIME_FORMAT).weekday() == 3, 'The next delivery date should be on a Thursday'
purchase_line_id_date_planned = datetime.datetime.strptime(procurement.purchase_line_id.date_planned, DEFAULT_SERVER_DATETIME_FORMAT).weekday()
assert purchase_line_id_date_planned == 3, 'Check it has been put on the purchase line also, got %d' %purchase_line_id_date_planned
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_stock_calendar
@@ -0,0 +1,101 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, timedelta
from odoo import fields
from odoo.tests import common
class TestsStockCalendar(common.TransactionCase):
def setUp(self):
super(TestsStockCalendar, self).setUp()
# Create a partner
self.stock_warehouse0_id = self.ref('stock.warehouse0')
self.purchase_route_warehouse0_buy_id = self.ref('purchase.route_warehouse0_buy')
self.stock_picking_type_out_id = self.ref('stock.picking_type_out')
self.stock_location_id = self.ref('stock.stock_location_stock')
self.stock_location_customer_id = self.ref('stock.stock_location_customers')
self.res_partner_id = self.env['res.partner'].create({
'name': 'Supplier',
'supplier': 1
})
# Create a calendar
self.resource_calendar_id = self.env['resource.calendar'].create({
'name': 'Calendar',
'attendance_ids': [(0, 0, {
'name': 'Thursday',
'dayofweek': '3',
'hour_from': 8,
'hour_to': 9
})]
})
# Create a product A with orderpoint with a calendar
self.calendar_product_id = self.env['product.product'].create({
'name': 'Calendar Product',
'seller_ids': [(0, 0, {
'name': self.res_partner_id.id,
'delay': 1
})],
'orderpoint_ids': [(0, 0, {
'name': 'Product A Truck',
'calendar_id': self.resource_calendar_id.id,
'product_min_qty': 0,
'product_max_qty': 10,
'warehouse_id': self.stock_warehouse0_id
})]
})
# Create delivery order with product A
self.pick_out_calendar = self._create_stock_picking('Delivery order for procurement', self.calendar_product_id.name, 3.00)
# Create other delivery order with product A that we will give a later date afterwards (date between the two)
self.pick_out_calendar2 = self._create_stock_picking('Delivery order for procurement2', 'stock_move_2', 4.00)
# Create other delivery order with product A that we will give an even later date, so it should not be taken into account
self.pick_out_calendar3 = self._create_stock_picking('Delivery order for procurement3', 'stock_move_3', 11.00)
def _create_stock_picking(self, pickname, movelinename, productqty):
return self.env['stock.picking'].create({
'name': pickname,
'partner_id': self.res_partner_id.id,
'picking_type_id': self.stock_picking_type_out_id,
'location_id': self.stock_location_id,
'location_dest_id': self.stock_location_customer_id,
'move_lines': [(0, 0, {'name': movelinename,
'product_id': self.calendar_product_id.id,
'product_uom': self.calendar_product_id.uom_id.id,
'product_uom_qty': productqty,
'location_id': self.stock_location_id,
'location_dest_id': self.stock_location_customer_id,
'procure_method': 'make_to_stock'})]
})
def test_stock_calendar(self):
# Put different dates in pickings, confirm them, run schedulers and check that the procurement generated by the
# orderpoint is taking the calendar into account. Make sure also that the purchase order creates a datetime.
self.calendar_product_id.write({'route_ids': [(4, self.purchase_route_warehouse0_buy_id)]})
today8 = datetime.now() + timedelta(days=7)
today21 = datetime.now() + timedelta(days=21)
self.pick_out_calendar2.move_lines.write({'date': fields.Datetime.to_string(today8), 'date_expected': fields.Datetime.to_string(today8)})
self.pick_out_calendar3.move_lines.write({'date_expected': fields.Datetime.to_string(today21), 'date': fields.Datetime.to_string(today21)})
# We need to confirm pickings and run the schedulers
self.pick_out_calendar.action_confirm()
self.pick_out_calendar2.action_confirm()
self.pick_out_calendar3.action_confirm()
# Check .procurements generated
Procurementorder = self.env['procurement.order']
Procurementorder.run_scheduler()
procurement = Procurementorder.search([('product_id', '=', self.calendar_product_id.id)], limit=1)
self.assertEqual(len(procurement), 1, 'should have one procurement')
self.assertEqual(procurement.product_qty, 17, 'It should have taken the two first pickings into account for the virtual stock for the orderpoint, not the third')
self.assertEqual(fields.Datetime.from_string(procurement.next_delivery_date).weekday(), 3, 'The next delivery date should be on a Thursday')
purchase_line_id_date_planned = fields.Datetime.from_string(procurement.purchase_line_id.date_planned).weekday()
self.assertEqual(purchase_line_id_date_planned, 3, 'Check it has been put on the purchase line also, got %d' % purchase_line_id_date_planned)
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- For testing purposes -->
<record id="view_procurement_form_stock_inherit" model="ir.ui.view">
<field name="name">procurement.order.form.stock.inherit</field>
<field name="model">procurement.order</field>
<field name="inherit_id" ref="procurement.procurement_form_view"/>
<field name="arch" type="xml">
<field name="state" position="before">
<button name="assign_group_date" string="Calculate dates according to Orderpoint" type="object"/>
</field>
</field>
</record>
</odoo>
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Procurement group to be used on the calendar attendance to match triggers and purchases-->
<record id="view_resource_calendar_attendance_tree_procurement_group" model="ir.ui.view">
<field name="name">resource.calendar.attendance.tree.procurement</field>
<field name="inherit_id" ref="resource.view_resource_calendar_attendance_tree"/>
<field name="model">resource.calendar.attendance</field>
<field name="arch" type="xml">
<field name="date_to" position="after">
<field name="group_id"/>
</field>
</field>
</record>
</odoo>
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="view_warehouse_orderpoint_form_inherit" model="ir.ui.view">
<field name="name">stock.warehouse.orderpoint.form.inherit</field>
<field name="model">stock.warehouse.orderpoint</field>
<field name="inherit_id" ref="stock.view_warehouse_orderpoint_form"/>
<field name="arch" type="xml">
<field name="company_id" position="after">
<field name="calendar_id"/>
<field name="purchase_calendar_id"/>
</field>
</field>
</record>
</data>
</odoo>
+1 -1
View File
@@ -22,7 +22,7 @@ internal transfer document is needed.
""",
'website': 'https://www.odoo.com/page/warehouse',
'depends': ['purchase', 'sale_stock'],
'data': ['stock_dropshipping.xml'],
'data': ['data/stock_data.xml'],
'test': [
'../account/test/account_minimal_test.xml',
'../stock_account/test/stock_valuation_account.xml',
@@ -1,16 +1,13 @@
<?xml version="1.0"?>
<openerp>
<odoo>
<data>
<!-- Sequence and picking type dropship-->
<record id="seq_picking_type_dropship" model="ir.sequence">
<field name="name">Dropship</field>
<field name="prefix">DS</field>
<field name="padding">5</field>
<field name="company_id" eval="False"/>
</record>
<record id="picking_type_dropship" model="stock.picking.type">
<field name="name">Dropship</field>
<field name="sequence_id" ref="seq_picking_type_dropship"/>
@@ -19,8 +16,7 @@
<field name="default_location_src_id" ref="stock.stock_location_suppliers"/>
<field name="default_location_dest_id" ref="stock.stock_location_customers"/>
</record>
<!-- Drop Shipping-->
<record id="route_drop_shipping" model='stock.location.route'>
<field name="name">Drop Shipping</field>
@@ -40,4 +36,4 @@
<field name="picking_type_id" ref="picking_type_dropship"/>
</record>
</data>
</openerp>
</odoo>
+1 -3
View File
@@ -1,6 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import product
import stock_landed_costs
import purchase_config_settings
import models
+3 -4
View File
@@ -18,10 +18,9 @@ This module allows you to easily add extra costs on pickings and decide the spli
],
'data': [
'security/ir.model.access.csv',
'stock_landed_costs_sequence.xml',
'product_view.xml',
'stock_landed_costs_view.xml',
'stock_landed_costs_data.xml',
'data/stock_landed_cost_data.xml',
'views/product_views.xml',
'views/stock_landed_cost_views.xml',
],
'test': [
'../account/test/account_minimal_test.xml',
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<odoo>
<data noupdate="1">
<!-- Stock landed costs - related subtypes for messaging / Chatter -->
<record id="mt_stock_landed_cost_open" model="mail.message.subtype">
<field name="name">Done</field>
@@ -12,6 +11,12 @@
<record model="res.groups" id="base.group_user">
<field name="implied_ids" eval="[(4, ref('stock_account.group_inventory_valuation'))]"/>
</record>
<record id="seq_stock_landed_costs" model="ir.sequence">
<field name="name">Stock Landed Costs</field>
<field name="code">stock.landed.cost</field>
<field name="prefix">LC/%(year)s/</field>
<field name="padding">4</field>
</record>
</data>
</openerp>
</odoo>
@@ -0,0 +1,6 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import product
import stock_landed_cost
import purchase_config_settings
@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
SPLIT_METHOD = [
('equal', 'Equal'),
('by_quantity', 'By Quantity'),
('by_current_cost_price', 'By Current Cost'),
('by_weight', 'By Weight'),
('by_volume', 'By Volume'),
]
class ProductTemplate(models.Model):
_inherit = "product.template"
landed_cost_ok = fields.Boolean('Landed Costs')
split_method = fields.Selection(
selection=SPLIT_METHOD, string='Split Method', default='equal',
help="Equal : Cost will be equally divided.\n"
"By Quantity : Cost will be divided according to product's quantity.\n"
"By Current cost : Cost will be divided according to product's current cost.\n"
"By Weight : Cost will be divided depending on its weight.\n"
"By Volume : Cost will be divided depending on its volume.")
@@ -1,8 +1,7 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp import api, models
from openerp.tools.translate import _
from odoo import api, models, _
class PurchaseConfigSettings(models.TransientModel):
@@ -12,13 +11,10 @@ class PurchaseConfigSettings(models.TransientModel):
@api.onchange('group_costing_method')
def onchange_costing_method(self):
if self.group_costing_method == 0:
self.group_costing_method = 1
return {
'warning': {
'title': _('Warning!'),
'message': _('Disabling the costing methods will prevent you to use the landed costs feature.'),
},
'value': {
'group_costing_method': 1
}
}
return {}
@@ -0,0 +1,395 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from collections import defaultdict
from odoo import api, fields, models, tools, _
from odoo.addons import decimal_precision as dp
from odoo.addons.stock_landed_costs.models import product
from odoo.exceptions import UserError
class LandedCost(models.Model):
_name = 'stock.landed.cost'
_description = 'Stock Landed Cost'
_inherit = 'mail.thread'
name = fields.Char(
'Name', default=lambda self: self.env['ir.sequence'].next_by_code('stock.landed.cost'),
copy=False, readonly=True, track_visibility='always')
date = fields.Date(
'Date', default=fields.Date.context_today,
copy=False, required=True, states={'done': [('readonly', True)]}, track_visibility='onchange')
picking_ids = fields.Many2many(
'stock.picking', string='Pickings',
copy=False, states={'done': [('readonly', True)]})
cost_lines = fields.One2many(
'stock.landed.cost.lines', 'cost_id', 'Cost Lines',
copy=True, states={'done': [('readonly', True)]})
valuation_adjustment_lines = fields.One2many(
'stock.valuation.adjustment.lines', 'cost_id', 'Valuation Adjustments',
states={'done': [('readonly', True)]})
description = fields.Text(
'Item Description', states={'done': [('readonly', True)]})
amount_total = fields.Float(
'Total', compute='_compute_total_amount',
digits=0, store=True, track_visibility='always')
state = fields.Selection([
('draft', 'Draft'),
('done', 'Posted'),
('cancel', 'Cancelled')], 'State', default='draft',
copy=False, readonly=True, track_visibility='onchange')
account_move_id = fields.Many2one(
'account.move', 'Journal Entry',
copy=False, readonly=True)
account_journal_id = fields.Many2one(
'account.journal', 'Account Journal',
required=True, states={'done': [('readonly', True)]})
@api.one
@api.depends('cost_lines.price_unit')
def _compute_total_amount(self):
self.amount_total = sum(line.price_unit for line in self.cost_lines)
@api.multi
def unlink(self):
self.button_cancel()
return super(LandedCost, self).unlink()
@api.multi
def _track_subtype(self, init_values):
if 'state' in init_values and self.state == 'done':
return 'stock_landed_costs.mt_stock_landed_cost_open'
return super(LandedCost, self)._track_subtype(init_values)
@api.multi
def button_cancel(self):
if any(cost.state == 'done' for cost in self):
raise UserError(
_('Validated landed costs cannot be cancelled, but you could create negative landed costs to reverse them'))
return self.write({'state': 'cancel'})
@api.multi
def button_validate(self):
if any(cost.state != 'draft' for cost in self):
raise UserError(_('Only draft landed costs can be validated'))
if any(not cost.valuation_adjustment_lines for cost in self):
raise UserError(_('No valuation adjustments lines. You should maybe recompute the landed costs.'))
if not self._check_sum():
raise UserError(_('Cost and adjustments lines do not match. You should maybe recompute the landed costs.'))
for cost in self:
move = self.env['account.move'].create({
'journal_id': cost.account_journal_id.id,
'date': cost.date,
'ref': cost.name
})
for line in cost.valuation_adjustment_lines.filtered(lambda line: line.move_id):
per_unit = line.final_cost / line.quantity
diff = per_unit - line.former_cost_per_unit
# If the precision required for the variable diff is larger than the accounting
# precision, inconsistencies between the stock valuation and the accounting entries
# may arise.
# For example, a landed cost of 15 divided in 13 units. If the products leave the
# stock one unit at a time, the amount related to the landed cost will correspond to
# round(15/13, 2)*13 = 14.95. To avoid this case, we split the quant in 12 + 1, then
# record the difference on the new quant.
# We need to make sure to able to extract at least one unit of the product. There is
# an arbitrary minimum quantity set to 2.0 from which we consider we can extract a
# unit and adapt the cost.
curr_rounding = line.move_id.company_id.currency_id.rounding
diff_rounded = tools.float_round(diff, precision_rounding=curr_rounding)
diff_correct = diff_rounded
quants = line.move_id.quant_ids.sorted(key=lambda r: r.qty, reverse=True)
quant_correct = False
if quants\
and tools.float_compare(quants[0].product_id.uom_id.rounding, 1.0, precision_digits=1) == 0\
and tools.float_compare(line.quantity * diff, line.quantity * diff_rounded, precision_rounding=curr_rounding) != 0\
and tools.float_compare(quants[0].qty, 2.0, precision_rounding=quants[0].product_id.uom_id.rounding) >= 0:
# Search for existing quant of quantity = 1.0 to avoid creating a new one
quant_correct = quants.filtered(lambda r: tools.float_compare(r.qty, 1.0, precision_rounding=quants[0].product_id.uom_id.rounding) == 0)
if not quant_correct:
quant_correct = quants[0]._quant_split(quants[0].qty - 1.0)
else:
quant_correct = quant_correct[0]
quants = quants - quant_correct
diff_correct += (line.quantity * diff) - (line.quantity * diff_rounded)
diff = diff_rounded
quant_dict = {}
for quant in quants:
quant_dict[quant] = quant.cost + diff
if quant_correct:
quant_dict[quant_correct] = quant_correct.cost + diff_correct
for quant, value in quant_dict.items():
quant.sudo().write({'cost': value})
qty_out = 0
for quant in line.move_id.quant_ids:
if quant.location_id.usage != 'internal':
qty_out += quant.qty
line._create_accounting_entries(move, qty_out)
cost.write({'state': 'done', 'account_move_id': move.id})
move.post()
return True
def _check_sum(self):
""" Check if each cost line its valuation lines sum to the correct amount
and if the overall total amount is correct also """
prec_digits = self.env['decimal.precision'].precision_get('Account')
for landed_cost in self:
total_amount = sum(landed_cost.valuation_adjustment_lines.mapped('additional_landed_cost'))
if not tools.float_compare(total_amount, landed_cost.amount_total, precision_digits=prec_digits) == 0:
return False
val_to_cost_lines = defaultdict(lambda: 0.0)
for val_line in landed_cost.valuation_adjustment_lines:
val_to_cost_lines[val_line.cost_line_id] += val_line.additional_landed_cost
if any(tools.float_compare(cost_line.price_unit, val_amount, precision_digits=prec_digits) != 0
for cost_line, val_amount in val_to_cost_lines.iteritems()):
return False
return True
def get_valuation_lines(self):
lines = []
for move in self.mapped('picking_ids').mapped('move_lines'):
# it doesn't make sense to make a landed cost for a product that isn't set as being valuated in real time at real cost
if move.product_id.valuation != 'real_time' or move.product_id.cost_method != 'real':
continue
vals = {
'product_id': move.product_id.id,
'move_id': move.id,
'quantity': move.product_qty,
'former_cost': sum(quant.cost * quant.qty for quant in move.quant_ids),
'weight': move.product_id.weight * move.product_qty,
'volume': move.product_id.volume * move.product_qty
}
lines.append(vals)
if not lines and self.mapped('picking_ids'):
raise UserError(_('The selected picking does not contain any move that would be impacted by landed costs. Landed costs are only possible for products configured in real time valuation with real price costing method. Please make sure it is the case, or you selected the correct picking'))
return lines
@api.multi
def compute_landed_cost(self):
AdjustementLines = self.env['stock.valuation.adjustment.lines']
AdjustementLines.search([('cost_id', 'in', self.ids)]).unlink()
digits = dp.get_precision('Product Price')(self._cr)
towrite_dict = {}
for cost in self.filtered(lambda cost: cost.picking_ids):
total_qty = 0.0
total_cost = 0.0
total_weight = 0.0
total_volume = 0.0
total_line = 0.0
all_val_line_values = cost.get_valuation_lines()
for val_line_values in all_val_line_values:
for cost_line in cost.cost_lines:
val_line_values.update({'cost_id': cost.id, 'cost_line_id': cost_line.id})
self.env['stock.valuation.adjustment.lines'].create(val_line_values)
total_qty += val_line_values.get('quantity', 0.0)
total_cost += val_line_values.get('former_cost', 0.0)
total_weight += val_line_values.get('weight', 0.0)
total_volume += val_line_values.get('volume', 0.0)
total_line += 1
for line in cost.cost_lines:
value_split = 0.0
for valuation in cost.valuation_adjustment_lines:
value = 0.0
if valuation.cost_line_id and valuation.cost_line_id.id == line.id:
if line.split_method == 'by_quantity' and total_qty:
per_unit = (line.price_unit / total_qty)
value = valuation.quantity * per_unit
elif line.split_method == 'by_weight' and total_weight:
per_unit = (line.price_unit / total_weight)
value = valuation.weight * per_unit
elif line.split_method == 'by_volume' and total_volume:
per_unit = (line.price_unit / total_volume)
value = valuation.volume * per_unit
elif line.split_method == 'equal':
value = (line.price_unit / total_line)
elif line.split_method == 'by_current_cost_price' and total_cost:
per_unit = (line.price_unit / total_cost)
value = valuation.former_cost * per_unit
else:
value = (line.price_unit / total_line)
if digits:
value = tools.float_round(value, precision_digits=digits[1], rounding_method='UP')
fnc = min if line.price_unit > 0 else max
value = fnc(value, line.price_unit - value_split)
value_split += value
if valuation.id not in towrite_dict:
towrite_dict[valuation.id] = value
else:
towrite_dict[valuation.id] += value
if towrite_dict:
for key, value in towrite_dict.items():
AdjustementLines.browse(key).write({'additional_landed_cost': value})
return True
class LandedCostLine(models.Model):
_name = 'stock.landed.cost.lines'
_description = 'Stock Landed Cost Lines'
name = fields.Char('Description')
cost_id = fields.Many2one(
'stock.landed.cost', 'Landed Cost',
required=True, ondelete='cascade')
product_id = fields.Many2one('product.product', 'Product', required=True)
price_unit = fields.Float('Cost', digits_compute=dp.get_precision('Product Price'), required=True)
split_method = fields.Selection(product.SPLIT_METHOD, string='Split Method', required=True)
account_id = fields.Many2one(
'account.account', 'Account',
domain=[('internal_type', '!=', 'view'), ('internal_type', '!=', 'closed'), ('deprecated', '=', False)])
@api.onchange('product_id')
def onchange_product_id(self):
if not self.product_id:
self.quantity = 0.0
self.name = self.product_id.name or ''
self.split_method = self.product_id.split_method or 'equal'
self.price_unit = self.product_id.standard_price or 0.0
self.account_id = self.product_id.property_account_expense_id.id or self.product_id.categ_id.property_account_expense_categ_id.id
class AdjustmentLines(models.Model):
_name = 'stock.valuation.adjustment.lines'
_description = 'Stock Valuation Adjustment Lines'
name = fields.Char(
'Description', compute='_compute_name', store=True)
cost_id = fields.Many2one(
'stock.landed.cost', 'Landed Cost',
ondelete='cascade', required=True)
cost_line_id = fields.Many2one(
'stock.landed.cost.lines', 'Cost Line', readonly=True)
move_id = fields.Many2one('stock.move', 'Stock Move', readonly=True)
product_id = fields.Many2one('product.product', 'Product', required=True)
quantity = fields.Float(
'Quantity', default=1.0,
digits_compute=dp.get_precision('Product Unit of Measure'), required=True)
weight = fields.Float(
'Weight', default=1.0,
digits_compute=dp.get_precision('Product Unit of Measure'))
volume = fields.Float(
'Volume', default=1.0,
digits_compute=dp.get_precision('Product Unit of Measure'))
former_cost = fields.Float(
'Former Cost', digits_compute=dp.get_precision('Product Price'))
former_cost_per_unit = fields.Float(
'Former Cost(Per Unit)', compute='_compute_former_cost_per_unit',
digits=0, store=True)
additional_landed_cost = fields.Float(
'Additional Landed Cost',
digits_compute=dp.get_precision('Product Price'))
final_cost = fields.Float(
'Final Cost', compute='_compute_final_cost',
digits=0, store=True)
@api.one
@api.depends('cost_line_id.name', 'product_id.code', 'product_id.name')
def _compute_name(self):
name = '%s - ' % (self.cost_line_id.name if self.cost_line_id else '')
self.name = name + (self.product_id.code or self.product_id.name or '')
@api.one
@api.depends('former_cost', 'quantity')
def _compute_former_cost_per_unit(self):
self.former_cost_per_unit = self.former_cost / (self.quantity or 1.0)
@api.one
@api.depends('former_cost', 'additional_landed_cost')
def _compute_final_cost(self):
self.final_cost = self.former_cost + self.additional_landed_cost
def _create_accounting_entries(self, move, qty_out):
# TDE CLEANME: product chosen for computation ?
cost_product = self.cost_line_id.product_id
if not cost_product:
return False
accounts = self.product_id.product_tmpl_id.get_product_accounts()
debit_account_id = accounts.get('stock_valuation') and accounts['stock_valuation'].id or False
already_out_account_id = accounts['stock_output'].id
credit_account_id = self.cost_line_id.account_id.id or cost_product.property_account_expense_id.id or cost_product.categ_id.property_account_expense_categ_id.id
if not credit_account_id:
raise UserError(_('Please configure Stock Expense Account for product: %s.') % (cost_product.name))
return self._create_account_move_line(move, credit_account_id, debit_account_id, qty_out, already_out_account_id)
def _create_account_move_line(self, move, credit_account_id, debit_account_id, qty_out, already_out_account_id):
"""
Generate the account.move.line values to track the landed cost.
Afterwards, for the goods that are already out of stock, we should create the out moves
"""
AccountMoveLine = self.env['account.move.line'].with_context(check_move_validity=False)
base_line = {
'name': self.name,
'move_id': move.id,
'product_id': self.product_id.id,
'quantity': self.quantity,
}
debit_line = dict(base_line, account_id=debit_account_id)
credit_line = dict(base_line, account_id=credit_account_id)
diff = self.additional_landed_cost
if diff > 0:
debit_line['debit'] = diff
credit_line['credit'] = diff
else:
# negative cost, reverse the entry
debit_line['credit'] = -diff
credit_line['debit'] = -diff
AccountMoveLine.create(debit_line)
AccountMoveLine.create(credit_line)
# Create account move lines for quants already out of stock
if qty_out > 0:
debit_line = dict(base_line,
name=(self.name + ": " + str(qty_out) + _(' already out')),
quantity=qty_out,
account_id=already_out_account_id)
credit_line = dict(base_line,
name=(self.name + ": " + str(qty_out) + _(' already out')),
quantity=qty_out,
account_id=debit_account_id)
diff = diff * qty_out / self.quantity
if diff > 0:
debit_line['debit'] = diff
credit_line['credit'] = diff
else:
# negative cost, reverse the entry
debit_line['credit'] = -diff
credit_line['debit'] = -diff
AccountMoveLine.create(debit_line)
AccountMoveLine.create(credit_line)
# TDE FIXME: oh dear
if self.env.user.company_id.anglo_saxon_accounting:
debit_line = dict(base_line,
name=(self.name + ": " + str(qty_out) + _(' already out')),
quantity=qty_out,
account_id=credit_account_id)
credit_line = dict(base_line,
name=(self.name + ": " + str(qty_out) + _(' already out')),
quantity=qty_out,
account_id=already_out_account_id)
if diff > 0:
debit_line['debit'] = diff
credit_line['credit'] = diff
else:
# negative cost, reverse the entry
debit_line['credit'] = -diff
credit_line['debit'] = -diff
AccountMoveLine.create(debit_line)
AccountMoveLine.create(credit_line)
move.assert_balanced()
return True
-29
View File
@@ -1,29 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp.osv import fields, osv
SPLIT_METHOD = [
('equal', 'Equal'),
('by_quantity', 'By Quantity'),
('by_current_cost_price', 'By Current Cost'),
('by_weight', 'By Weight'),
('by_volume', 'By Volume'),
]
class product_template(osv.osv):
_inherit = "product.template"
_columns = {
'landed_cost_ok': fields.boolean('Landed Costs'),
'split_method': fields.selection(SPLIT_METHOD, 'Split Method', help="Equal : Cost will be equally divided.\n"\
"By Quantity : Cost will be divided according to product's quantity.\n"\
"By Current cost : Cost will be divided according to product's current cost.\n"\
"By Weight : Cost will be divided depending on its weight.\n"\
"By Volume : Cost will be divided depending on its volume."),
}
_defaults = {
'landed_cost_ok': False,
'split_method': 'equal',
}
@@ -1,407 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from openerp.osv import fields, osv
import openerp.addons.decimal_precision as dp
from openerp.tools import float_compare, float_round
from openerp.tools.translate import _
import product
from openerp import SUPERUSER_ID
from openerp.exceptions import UserError
class stock_landed_cost(osv.osv):
_name = 'stock.landed.cost'
_description = 'Stock Landed Cost'
_inherit = 'mail.thread'
def _total_amount(self, cr, uid, ids, name, args, context=None):
result = {}
for cost in self.browse(cr, uid, ids, context=context):
total = 0.0
for line in cost.cost_lines:
total += line.price_unit
result[cost.id] = total
return result
def _get_cost_line(self, cr, uid, ids, context=None):
cost_to_recompute = []
for line in self.pool.get('stock.landed.cost.lines').browse(cr, uid, ids, context=context):
cost_to_recompute.append(line.cost_id.id)
return cost_to_recompute
def get_valuation_lines(self, cr, uid, ids, picking_ids=None, context=None):
picking_obj = self.pool.get('stock.picking')
lines = []
if not picking_ids:
return lines
for picking in picking_obj.browse(cr, uid, picking_ids):
for move in picking.move_lines:
#it doesn't make sense to make a landed cost for a product that isn't set as being valuated in real time at real cost
if move.product_id.valuation != 'real_time' or move.product_id.cost_method != 'real':
continue
total_cost = 0.0
weight = move.product_id and move.product_id.weight * move.product_qty
volume = move.product_id and move.product_id.volume * move.product_qty
for quant in move.quant_ids:
total_cost += quant.cost * quant.qty
vals = dict(product_id=move.product_id.id, move_id=move.id, quantity=move.product_qty, former_cost=total_cost, weight=weight, volume=volume)
lines.append(vals)
if not lines:
raise UserError(_('The selected picking does not contain any move that would be impacted by landed costs. Landed costs are only possible for products configured in real time valuation with real price costing method. Please make sure it is the case, or you selected the correct picking'))
return lines
_columns = {
'name': fields.char('Name', track_visibility='always', readonly=True, copy=False),
'date': fields.date('Date', required=True, states={'done': [('readonly', True)]}, track_visibility='onchange', copy=False),
'picking_ids': fields.many2many('stock.picking', string='Pickings', states={'done': [('readonly', True)]}, copy=False),
'cost_lines': fields.one2many('stock.landed.cost.lines', 'cost_id', 'Cost Lines', states={'done': [('readonly', True)]}, copy=True),
'valuation_adjustment_lines': fields.one2many('stock.valuation.adjustment.lines', 'cost_id', 'Valuation Adjustments', states={'done': [('readonly', True)]}),
'description': fields.text('Item Description', states={'done': [('readonly', True)]}),
'amount_total': fields.function(_total_amount, type='float', string='Total', digits=0,
store={
'stock.landed.cost': (lambda self, cr, uid, ids, c={}: ids, ['cost_lines'], 20),
'stock.landed.cost.lines': (_get_cost_line, ['price_unit', 'quantity', 'cost_id'], 20),
}, track_visibility='always'
),
'state': fields.selection([('draft', 'Draft'), ('done', 'Posted'), ('cancel', 'Cancelled')], 'State', readonly=True, track_visibility='onchange', copy=False),
'account_move_id': fields.many2one('account.move', 'Journal Entry', readonly=True, copy=False),
'account_journal_id': fields.many2one('account.journal', 'Account Journal', required=True, states={'done': [('readonly', True)]}),
}
_defaults = {
'name': lambda obj, cr, uid, context: obj.pool.get('ir.sequence').next_by_code(cr, uid, 'stock.landed.cost'),
'state': 'draft',
'date': fields.date.context_today,
}
def _create_accounting_entries(self, cr, uid, line, move_id, qty_out, context=None):
product_obj = self.pool.get('product.template')
cost_product = line.cost_line_id and line.cost_line_id.product_id
if not cost_product:
return False
accounts = product_obj.browse(cr, uid, line.product_id.product_tmpl_id.id, context=context).get_product_accounts()
debit_account_id = accounts.get('stock_valuation', False) and accounts['stock_valuation'].id or False
already_out_account_id = accounts['stock_output'].id
credit_account_id = line.cost_line_id.account_id.id or cost_product.property_account_expense_id.id or cost_product.categ_id.property_account_expense_categ_id.id
if not credit_account_id:
raise UserError(_('Please configure Stock Expense Account for product: %s.') % (cost_product.name))
return self._create_account_move_line(cr, uid, line, move_id, credit_account_id, debit_account_id, qty_out, already_out_account_id, context=context)
def _create_account_move_line(self, cr, uid, line, move_id, credit_account_id, debit_account_id, qty_out, already_out_account_id, context=None):
"""
Generate the account.move.line values to track the landed cost.
Afterwards, for the goods that are already out of stock, we should create the out moves
"""
aml_obj = self.pool.get('account.move.line')
user_obj = self.pool.get('res.users')
if context is None:
context = {}
ctx = context.copy()
ctx['check_move_validity'] = False
base_line = {
'name': line.name,
'move_id': move_id,
'product_id': line.product_id.id,
'quantity': line.quantity,
}
debit_line = dict(base_line, account_id=debit_account_id)
credit_line = dict(base_line, account_id=credit_account_id)
diff = line.additional_landed_cost
if diff > 0:
debit_line['debit'] = diff
credit_line['credit'] = diff
else:
# negative cost, reverse the entry
debit_line['credit'] = -diff
credit_line['debit'] = -diff
aml_obj.create(cr, uid, debit_line, context=ctx)
aml_obj.create(cr, uid, credit_line, context=ctx)
#Create account move lines for quants already out of stock
if qty_out > 0:
debit_line = dict(base_line,
name=(line.name + ": " + str(qty_out) + _(' already out')),
quantity=qty_out,
account_id=already_out_account_id)
credit_line = dict(base_line,
name=(line.name + ": " + str(qty_out) + _(' already out')),
quantity=qty_out,
account_id=debit_account_id)
diff = diff * qty_out / line.quantity
if diff > 0:
debit_line['debit'] = diff
credit_line['credit'] = diff
else:
# negative cost, reverse the entry
debit_line['credit'] = -diff
credit_line['debit'] = -diff
aml_obj.create(cr, uid, debit_line, context=ctx)
aml_obj.create(cr, uid, credit_line, context=ctx)
if user_obj.browse(cr, uid, [uid], context=context).company_id.anglo_saxon_accounting:
debit_line = dict(base_line,
name=(line.name + ": " + str(qty_out) + _(' already out')),
quantity=qty_out,
account_id=credit_account_id)
credit_line = dict(base_line,
name=(line.name + ": " + str(qty_out) + _(' already out')),
quantity=qty_out,
account_id=already_out_account_id)
if diff > 0:
debit_line['debit'] = diff
credit_line['credit'] = diff
else:
# negative cost, reverse the entry
debit_line['credit'] = -diff
credit_line['debit'] = -diff
aml_obj.create(cr, uid, debit_line, context=ctx)
aml_obj.create(cr, uid, credit_line, context=ctx)
self.pool.get('account.move').assert_balanced(cr, uid, [move_id], context=context)
return True
def _create_account_move(self, cr, uid, cost, context=None):
vals = {
'journal_id': cost.account_journal_id.id,
'date': cost.date,
'ref': cost.name
}
return self.pool.get('account.move').create(cr, uid, vals, context=context)
def _check_sum(self, cr, uid, landed_cost, context=None):
"""
Will check if each cost line its valuation lines sum to the correct amount
and if the overall total amount is correct also
"""
costcor = {}
tot = 0
for valuation_line in landed_cost.valuation_adjustment_lines:
if costcor.get(valuation_line.cost_line_id):
costcor[valuation_line.cost_line_id] += valuation_line.additional_landed_cost
else:
costcor[valuation_line.cost_line_id] = valuation_line.additional_landed_cost
tot += valuation_line.additional_landed_cost
prec = self.pool['decimal.precision'].precision_get(cr, uid, 'Account')
# float_compare returns 0 for equal amounts
res = not bool(float_compare(tot, landed_cost.amount_total, precision_digits=prec))
for costl in costcor.keys():
if float_compare(costcor[costl], costl.price_unit, precision_digits=prec):
res = False
return res
def button_validate(self, cr, uid, ids, context=None):
quant_obj = self.pool.get('stock.quant')
for cost in self.browse(cr, uid, ids, context=context):
if cost.state != 'draft':
raise UserError(_('Only draft landed costs can be validated'))
if not cost.valuation_adjustment_lines or not self._check_sum(cr, uid, cost, context=context):
raise UserError(_('You cannot validate a landed cost which has no valid valuation adjustments lines. Did you click on Compute?'))
move_id = self._create_account_move(cr, uid, cost, context=context)
for line in cost.valuation_adjustment_lines:
if not line.move_id:
continue
per_unit = line.final_cost / line.quantity
diff = per_unit - line.former_cost_per_unit
# If the precision required for the variable diff is larger than the accounting
# precision, inconsistencies between the stock valuation and the accounting entries
# may arise.
# For example, a landed cost of 15 divided in 13 units. If the products leave the
# stock one unit at a time, the amount related to the landed cost will correspond to
# round(15/13, 2)*13 = 14.95. To avoid this case, we split the quant in 12 + 1, then
# record the difference on the new quant.
# We need to make sure to able to extract at least one unit of the product. There is
# an arbitrary minimum quantity set to 2.0 from which we consider we can extract a
# unit and adapt the cost.
curr_rounding = line.move_id.company_id.currency_id.rounding
diff_rounded = float_round(diff, precision_rounding=curr_rounding)
diff_correct = diff_rounded
quants = line.move_id.quant_ids.sorted(key=lambda r: r.qty, reverse=True)
quant_correct = False
if quants\
and float_compare(quants[0].product_id.uom_id.rounding, 1.0, precision_digits=1) == 0\
and float_compare(line.quantity * diff, line.quantity * diff_rounded, precision_rounding=curr_rounding) != 0\
and float_compare(quants[0].qty, 2.0, precision_rounding=quants[0].product_id.uom_id.rounding) >= 0:
# Search for existing quant of quantity = 1.0 to avoid creating a new one
quant_correct = quants.filtered(lambda r: float_compare(r.qty, 1.0, precision_rounding=quants[0].product_id.uom_id.rounding) == 0)
if not quant_correct:
quant_correct = quants[0]._quant_split(quants[0].qty - 1.0)
else:
quant_correct = quant_correct[0]
quants = quants - quant_correct
diff_correct += (line.quantity * diff) - (line.quantity * diff_rounded)
diff = diff_rounded
quant_dict = {}
for quant in quants:
quant_dict[quant.id] = quant.cost + diff
if quant_correct:
quant_dict[quant_correct.id] = quant_correct.cost + diff_correct
for key, value in quant_dict.items():
quant_obj.write(cr, SUPERUSER_ID, key, {'cost': value}, context=context)
qty_out = 0
for quant in line.move_id.quant_ids:
if quant.location_id.usage != 'internal':
qty_out += quant.qty
self._create_accounting_entries(cr, uid, line, move_id, qty_out, context=context)
self.write(cr, uid, cost.id, {'state': 'done', 'account_move_id': move_id}, context=context)
self.pool.get('account.move').post(cr, uid, [move_id], context=context)
return True
def button_cancel(self, cr, uid, ids, context=None):
cost = self.browse(cr, uid, ids, context=context)
if cost.state == 'done':
raise UserError(_('Validated landed costs cannot be cancelled, '
'but you could create negative landed costs to reverse them'))
return cost.write({'state': 'cancel'})
def unlink(self, cr, uid, ids, context=None):
# cancel or raise first
self.button_cancel(cr, uid, ids, context)
return super(stock_landed_cost, self).unlink(cr, uid, ids, context=context)
def compute_landed_cost(self, cr, uid, ids, context=None):
line_obj = self.pool.get('stock.valuation.adjustment.lines')
unlink_ids = line_obj.search(cr, uid, [('cost_id', 'in', ids)], context=context)
line_obj.unlink(cr, uid, unlink_ids, context=context)
digits = dp.get_precision('Product Price')(cr)
towrite_dict = {}
for cost in self.browse(cr, uid, ids, context=None):
if not cost.picking_ids:
continue
picking_ids = [p.id for p in cost.picking_ids]
total_qty = 0.0
total_cost = 0.0
total_weight = 0.0
total_volume = 0.0
total_line = 0.0
vals = self.get_valuation_lines(cr, uid, [cost.id], picking_ids=picking_ids, context=context)
for v in vals:
for line in cost.cost_lines:
v.update({'cost_id': cost.id, 'cost_line_id': line.id})
self.pool.get('stock.valuation.adjustment.lines').create(cr, uid, v, context=context)
total_qty += v.get('quantity', 0.0)
total_cost += v.get('former_cost', 0.0)
total_weight += v.get('weight', 0.0)
total_volume += v.get('volume', 0.0)
total_line += 1
for line in cost.cost_lines:
value_split = 0.0
for valuation in cost.valuation_adjustment_lines:
value = 0.0
if valuation.cost_line_id and valuation.cost_line_id.id == line.id:
if line.split_method == 'by_quantity' and total_qty:
per_unit = (line.price_unit / total_qty)
value = valuation.quantity * per_unit
elif line.split_method == 'by_weight' and total_weight:
per_unit = (line.price_unit / total_weight)
value = valuation.weight * per_unit
elif line.split_method == 'by_volume' and total_volume:
per_unit = (line.price_unit / total_volume)
value = valuation.volume * per_unit
elif line.split_method == 'equal':
value = (line.price_unit / total_line)
elif line.split_method == 'by_current_cost_price' and total_cost:
per_unit = (line.price_unit / total_cost)
value = valuation.former_cost * per_unit
else:
value = (line.price_unit / total_line)
if digits:
value = float_round(value, precision_digits=digits[1], rounding_method='UP')
fnc = min if line.price_unit > 0 else max
value = fnc(value, line.price_unit - value_split)
value_split += value
if valuation.id not in towrite_dict:
towrite_dict[valuation.id] = value
else:
towrite_dict[valuation.id] += value
if towrite_dict:
for key, value in towrite_dict.items():
line_obj.write(cr, uid, key, {'additional_landed_cost': value}, context=context)
return True
def _track_subtype(self, cr, uid, ids, init_values, context=None):
record = self.browse(cr, uid, ids[0], context=context)
if 'state' in init_values and record.state == 'done':
return 'stock_landed_costs.mt_stock_landed_cost_open'
return super(stock_landed_cost, self)._track_subtype(cr, uid, ids, init_values, context=context)
class stock_landed_cost_lines(osv.osv):
_name = 'stock.landed.cost.lines'
_description = 'Stock Landed Cost Lines'
def onchange_product_id(self, cr, uid, ids, product_id=False, context=None):
result = {}
if not product_id:
return {'value': {'quantity': 0.0, 'price_unit': 0.0}}
product = self.pool.get('product.product').browse(cr, uid, product_id, context=context)
result['name'] = product.name
result['split_method'] = product.split_method
result['price_unit'] = product.standard_price
result['account_id'] = product.property_account_expense_id and product.property_account_expense_id.id or product.categ_id.property_account_expense_categ_id.id
return {'value': result}
_columns = {
'name': fields.char('Description'),
'cost_id': fields.many2one('stock.landed.cost', 'Landed Cost', required=True, ondelete='cascade'),
'product_id': fields.many2one('product.product', 'Product', required=True),
'price_unit': fields.float('Cost', required=True, digits_compute=dp.get_precision('Product Price')),
'split_method': fields.selection(product.SPLIT_METHOD, string='Split Method', required=True),
'account_id': fields.many2one('account.account', 'Account', domain=[('internal_type', '!=', 'view'), ('internal_type', '!=', 'closed'), ('deprecated', '=', False)]),
}
class stock_valuation_adjustment_lines(osv.osv):
_name = 'stock.valuation.adjustment.lines'
_description = 'Stock Valuation Adjustment Lines'
def _amount_final(self, cr, uid, ids, name, args, context=None):
result = {}
for line in self.browse(cr, uid, ids, context=context):
result[line.id] = {
'former_cost_per_unit': 0.0,
'final_cost': 0.0,
}
result[line.id]['former_cost_per_unit'] = (line.former_cost / line.quantity if line.quantity else 1.0)
result[line.id]['final_cost'] = (line.former_cost + line.additional_landed_cost)
return result
def _get_name(self, cr, uid, ids, name, arg, context=None):
res = {}
for line in self.browse(cr, uid, ids, context=context):
res[line.id] = line.product_id.code or line.product_id.name or ''
if line.cost_line_id:
res[line.id] += ' - ' + line.cost_line_id.name
return res
_columns = {
'name': fields.function(_get_name, type='char', string='Description', store=True),
'cost_id': fields.many2one('stock.landed.cost', 'Landed Cost', required=True, ondelete='cascade'),
'cost_line_id': fields.many2one('stock.landed.cost.lines', 'Cost Line', readonly=True),
'move_id': fields.many2one('stock.move', 'Stock Move', readonly=True),
'product_id': fields.many2one('product.product', 'Product', required=True),
'quantity': fields.float('Quantity', digits_compute=dp.get_precision('Product Unit of Measure'), required=True),
'weight': fields.float('Weight', digits_compute=dp.get_precision('Product Unit of Measure')),
'volume': fields.float('Volume', digits_compute=dp.get_precision('Product Unit of Measure')),
'former_cost': fields.float('Former Cost', digits_compute=dp.get_precision('Product Price')),
'former_cost_per_unit': fields.function(_amount_final, multi='cost', string='Former Cost(Per Unit)', type='float', store=True, digits=0),
'additional_landed_cost': fields.float('Additional Landed Cost', digits_compute=dp.get_precision('Product Price')),
'final_cost': fields.function(_amount_final, multi='cost', string='Final Cost', type='float', store=True, digits=0),
}
_defaults = {
'quantity': 1.0,
'weight': 1.0,
'volume': 1.0,
}
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<data noupdate="1">
<record id="seq_stock_landed_costs" model="ir.sequence">
<field name="name">Stock Landed Costs</field>
<field name="code">stock.landed.cost</field>
<field name="prefix">LC/%(year)s/</field>
<field name="padding">4</field>
</record>
</data>
</openerp>
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<openerp>
<odoo>
<data>
<record id="view_product_landed_cost_form" model="ir.ui.view">
<field name="name">product.template.landed.cost.form</field>
<field name="model">product.template</field>
@@ -15,6 +14,5 @@
</group>
</field>
</record>
</data>
</openerp>
</odoo>
@@ -1,8 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<odoo>
<data>
<!-- Stock Landed Cost Form View -->
<!-- STOCK.LANDED.COST -->
<record id='view_stock_landed_cost_form' model='ir.ui.view'>
<field name="name">stock.landed.cost.form</field>
<field name="model">stock.landed.cost</field>
@@ -40,7 +39,8 @@
<group>
<group>
<field name="product_id"
on_change="onchange_product_id(product_id)" domain="[('landed_cost_ok', '=', True)]" context="{'default_landed_cost_ok': True, 'form_view_ref':'stock_landed_costs.view_stock_landed_cost_type_form'}"/>
domain="[('landed_cost_ok', '=', True)]"
context="{'default_landed_cost_ok': True, 'form_view_ref':'stock_landed_costs.view_stock_landed_cost_type_form'}"/>
<field name="price_unit"/>
</group>
<group>
@@ -52,7 +52,9 @@
<field name="name"/>
</form>
<tree string="Cost Lines" editable="bottom">
<field name="product_id" on_change="onchange_product_id(product_id)" domain="[('landed_cost_ok', '=', True)]" context="{'default_landed_cost_ok': True, 'form_view_ref':'stock_landed_costs.view_stock_landed_cost_type_form'}"/>
<field name="product_id"
domain="[('landed_cost_ok', '=', True)]"
context="{'default_landed_cost_ok': True, 'form_view_ref':'stock_landed_costs.view_stock_landed_cost_type_form'}"/>
<field name="name"/>
<field name="account_id"/>
<field name="split_method"/>
@@ -99,7 +101,6 @@
</field>
</record>
<!-- Stock Landed Cost Tree View -->
<record id='view_stock_landed_cost_tree' model='ir.ui.view'>
<field name="name">stock.landed.cost.tree</field>
<field name="model">stock.landed.cost</field>
@@ -112,7 +113,6 @@
</field>
</record>
<!-- Stock Landed Cost Search View -->
<record id="view_stock_landed_cost_search" model="ir.ui.view">
<field name="name">stock.landed.cost.search</field>
<field name="model">stock.landed.cost</field>
@@ -131,7 +131,6 @@
</field>
</record>
<!-- Stock Landed Cost Action -->
<record id='action_stock_landed_cost' model='ir.actions.act_window'>
<field name="name">Landed Costs</field>
<field name="res_model">stock.landed.cost</field>
@@ -145,10 +144,9 @@
</field>
</record>
<!-- Stock Landed Cost Menu -->
<menuitem action="action_stock_landed_cost" name="Landed Costs" parent="stock.menu_stock_inventory_control" id="menu_stock_landed_cost" sequence="99"/>
<!-- Stock Landed Cost Type View -->
<!-- STOCK.LANDED.COST.TYPE -->
<record id="view_stock_landed_cost_type_form" model="ir.ui.view">
<field name="name">stock.landed.cost.type.form</field>
<field name="model">product.product</field>
@@ -200,7 +198,6 @@
</field>
</record>
<!-- Stock Landed Cost Type Tree View -->
<record id="stock_landed_cost_tree_view" model="ir.ui.view">
<field name="name">product.product.tree</field>
<field name="model">product.product</field>
@@ -212,6 +209,7 @@
</tree>
</field>
</record>
<record id="stock_landed_cost_type_action" model="ir.actions.act_window">
<field name="name">Products</field>
<field name="type">ir.actions.act_window</field>
@@ -245,4 +243,4 @@
<menuitem action="stock_landed_cost_type_action" name="Landed Cost Type" parent="stock.menu_warehouse_config" id="menu_stock_landed_cost_type" sequence="3"/>
</data>
</openerp>
</odoo>
+10 -10
View File
@@ -6,19 +6,19 @@
'version': '1.0',
'category': 'Warehouse',
'description': """
This module adds the picking wave option in warehouse management.
=================================================================
This module adds the picking wave option in warehouse management
================================================================
""",
'website': 'https://www.odoo.com/page/warehouse',
'depends': ['stock'],
'data': ['security/ir.model.access.csv',
'views/stock_picking_wave_views.xml',
'data/stock_picking_wave_data.xml',
'data/ir_sequence_data_picking_wave.xml',
'wizard/stock_picking_to_wave_views.xml',
],
'data': [
'security/ir.model.access.csv',
'views/stock_picking_wave_views.xml',
'data/stock_picking_wave_data.xml',
'wizard/stock_picking_to_wave_views.xml',
],
'demo': [
'data/stock_picking_wave_demo.xml',
],
'data/stock_picking_wave_demo.xml',
],
'installable': True,
}
@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Sequences for picking wave -->
<record id="seq_picking_wave" model="ir.sequence">
<field name="name">Picking Wave</field>
<field name="code">picking.wave</field>
<field name="prefix">Wave/</field>
<field name="padding">5</field>
<field name="company_id" eval="False"/>
</record>
</data>
</odoo>
@@ -7,4 +7,12 @@
<field name="default" eval="False"/>
<field name="description">Stage Changed</field>
</record>
<record id="seq_picking_wave" model="ir.sequence">
<field name="name">Picking Wave</field>
<field name="code">picking.wave</field>
<field name="prefix">Wave/</field>
<field name="padding">5</field>
<field name="company_id" eval="False"/>
</record>
</odoo>
@@ -28,6 +28,12 @@ class StockPickingWave(models.Model):
('cancel', 'Cancelled')], default='draft',
copy=False, track_visibility='onchange', required=True)
@api.model
def create(self, vals):
if vals.get('name', '/') == '/':
vals['name'] = self.env['ir.sequence'].next_by_code('picking.wave') or '/'
return super(StockPickingWave, self).create(vals)
@api.multi
def confirm_picking(self):
pickings_todo = self.mapped('picking_ids')
@@ -46,12 +52,6 @@ class StockPickingWave(models.Model):
raise UserError(_('Nothing to print.'))
return self.env["report"].with_context(active_ids=pickings.ids, active_model='stock.picking').get_action(pickings, 'stock.report_picking')
@api.model
def create(self, vals):
if vals.get('name', '/') == '/':
vals['name'] = self.env['ir.sequence'].next_by_code('picking.wave') or '/'
return super(StockPickingWave, self).create(vals)
@api.multi
def done(self):
pickings = self.mapped('picking_ids').filtered(lambda picking: picking.state not in ('cancel', 'done'))
@@ -12,7 +12,7 @@ class StockPickingToWave(models.TransientModel):
@api.multi
def attach_pickings(self):
#use active_ids to add picking line to the selected wave
# use active_ids to add picking line to the selected wave
self.ensure_one()
picking_ids = self.env.context.get('active_ids')
return self.env['stock.picking'].browse(picking_ids).write({'wave_id': self.wave_id.id})
+2 -2
View File
@@ -500,7 +500,7 @@ class YamlInterpreter(object):
return {
key: val
for key, val in record_dict.iteritems()
if (key in model._columns or key in model._inherit_fields)
if (key in model._columns or key in model._inherit_fields or model._fields[key].inverse)
}
def process_ref(self, node, field=None):
@@ -518,7 +518,7 @@ class YamlInterpreter(object):
if node.use:
value = [inst[node.use] for inst in instances]
else:
value = instance.ids
value = instances.ids
elif node.id:
if field and field.type == 'reference':
record = self.get_record(node.id)