diff --git a/addons/product/product.py b/addons/product/product.py index df40b9a51e1..481a3838084 100644 --- a/addons/product/product.py +++ b/addons/product/product.py @@ -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 = {} diff --git a/addons/purchase/purchase.py b/addons/purchase/purchase.py index 10da6aacf52..f91a511dbcd 100644 --- a/addons/purchase/purchase.py +++ b/addons/purchase/purchase.py @@ -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() diff --git a/addons/sale_stock/__init__.py b/addons/sale_stock/__init__.py index f6a0e26645c..2609681a938 100644 --- a/addons/sale_stock/__init__.py +++ b/addons/sale_stock/__init__.py @@ -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 diff --git a/addons/sale_stock/__openerp__.py b/addons/sale_stock/__openerp__.py index d26776813fa..6acfb6a50b9 100644 --- a/addons/sale_stock/__openerp__.py +++ b/addons/sale_stock/__openerp__.py @@ -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, } diff --git a/addons/sale_stock/sale_stock_demo.xml b/addons/sale_stock/data/sale_order_demo.xml similarity index 100% rename from addons/sale_stock/sale_stock_demo.xml rename to addons/sale_stock/data/sale_order_demo.xml diff --git a/addons/sale_stock/models/__init__.py b/addons/sale_stock/models/__init__.py new file mode 100644 index 00000000000..961eddcf23b --- /dev/null +++ b/addons/sale_stock/models/__init__.py @@ -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 diff --git a/addons/sale_stock/models/account_invoice.py b/addons/sale_stock/models/account_invoice.py new file mode 100644 index 00000000000..4bd9efe8021 --- /dev/null +++ b/addons/sale_stock/models/account_invoice.py @@ -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 \ No newline at end of file diff --git a/addons/sale_stock/models/procurement.py b/addons/sale_stock/models/procurement.py new file mode 100644 index 00000000000..93c07aaf7c0 --- /dev/null +++ b/addons/sale_stock/models/procurement.py @@ -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 diff --git a/addons/sale_stock/models/product.py b/addons/sale_stock/models/product.py new file mode 100644 index 00000000000..a3c3b678cbf --- /dev/null +++ b/addons/sale_stock/models/product.py @@ -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() diff --git a/addons/sale_stock/company.py b/addons/sale_stock/models/res_company.py similarity index 74% rename from addons/sale_stock/company.py rename to addons/sale_stock/models/res_company.py index 7b1e382f1aa..1677868c718 100644 --- a/addons/sale_stock/company.py +++ b/addons/sale_stock/models/res_company.py @@ -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.") diff --git a/addons/sale_stock/res_config.py b/addons/sale_stock/models/sale_config_settings.py similarity index 90% rename from addons/sale_stock/res_config.py rename to addons/sale_stock/models/sale_config_settings.py index ef3cf1af33e..32baad15126 100644 --- a/addons/sale_stock/res_config.py +++ b/addons/sale_stock/models/sale_config_settings.py @@ -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() diff --git a/addons/sale_stock/sale_stock.py b/addons/sale_stock/models/sale_order.py similarity index 54% rename from addons/sale_stock/sale_stock.py rename to addons/sale_stock/models/sale_order.py index f0bc955afb6..57ba77d43b5 100644 --- a/addons/sale_stock/sale_stock.py +++ b/addons/sale_stock/models/sale_order.py @@ -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() diff --git a/addons/sale_stock/models/stock.py b/addons/sale_stock/models/stock.py new file mode 100644 index 00000000000..94693801023 --- /dev/null +++ b/addons/sale_stock/models/stock.py @@ -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') diff --git a/addons/sale_stock/report/sale_order_report_templates.xml b/addons/sale_stock/report/sale_order_report_templates.xml new file mode 100644 index 00000000000..4d27dc34485 --- /dev/null +++ b/addons/sale_stock/report/sale_order_report_templates.xml @@ -0,0 +1,11 @@ + + + + \ No newline at end of file diff --git a/addons/sale_stock/report/sale_report.py b/addons/sale_stock/report/sale_report.py index 3475684df44..7220f045e53 100644 --- a/addons/sale_stock/report/sale_report.py +++ b/addons/sale_stock/report/sale_report.py @@ -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 diff --git a/addons/sale_stock/views/report_deliveryslip.xml b/addons/sale_stock/report/stock_report_deliveryslip.xml similarity index 100% rename from addons/sale_stock/views/report_deliveryslip.xml rename to addons/sale_stock/report/stock_report_deliveryslip.xml diff --git a/addons/sale_stock/sale_stock_demo.yml b/addons/sale_stock/sale_stock_demo.yml deleted file mode 100644 index 66df6e9643a..00000000000 --- a/addons/sale_stock/sale_stock_demo.yml +++ /dev/null @@ -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') diff --git a/addons/sale_stock/sale_stock_view.xml b/addons/sale_stock/sale_stock_view.xml deleted file mode 100644 index ea4f82074bb..00000000000 --- a/addons/sale_stock/sale_stock_view.xml +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - sale.order.form.sale.stock - sale.order - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - sale.order.line.tree.sale.stock.location - - sale.order.line - - - - - - - - - stock.return.picking.sale.stock.form - - stock.return.picking - - - - - - - - - - - diff --git a/addons/sale_stock/account_invoice_view.xml b/addons/sale_stock/views/account_invoice_views.xml similarity index 100% rename from addons/sale_stock/account_invoice_view.xml rename to addons/sale_stock/views/account_invoice_views.xml diff --git a/addons/sale_stock/company_view.xml b/addons/sale_stock/views/res_company_views.xml similarity index 96% rename from addons/sale_stock/company_view.xml rename to addons/sale_stock/views/res_company_views.xml index 4866d30d6d6..96b29e66925 100644 --- a/addons/sale_stock/company_view.xml +++ b/addons/sale_stock/views/res_company_views.xml @@ -1,5 +1,5 @@ - + res.company.mrp.config @@ -13,4 +13,4 @@ - + diff --git a/addons/sale_stock/res_config_view.xml b/addons/sale_stock/views/sale_config_settings_views.xml similarity index 100% rename from addons/sale_stock/res_config_view.xml rename to addons/sale_stock/views/sale_config_settings_views.xml diff --git a/addons/sale_stock/views/sale_order_views.xml b/addons/sale_stock/views/sale_order_views.xml new file mode 100644 index 00000000000..1f5a1c24e2c --- /dev/null +++ b/addons/sale_stock/views/sale_order_views.xml @@ -0,0 +1,50 @@ + + + + + sale.order.form.sale.stock + sale.order + + + + + + + + + + + + + + + + + + + + + + + + + + + + sale.order.line.tree.sale.stock.location + + sale.order.line + + + + + + + + diff --git a/addons/sale_stock/stock_view.xml b/addons/sale_stock/views/stock_views.xml similarity index 63% rename from addons/sale_stock/stock_view.xml rename to addons/sale_stock/views/stock_views.xml index 5ac1c6aacad..894fbd91b66 100644 --- a/addons/sale_stock/stock_view.xml +++ b/addons/sale_stock/views/stock_views.xml @@ -1,5 +1,5 @@ - + + + stock.return.picking.sale.stock.form + + stock.return.picking + + + + + + + - + diff --git a/addons/stock_account/__init__.py b/addons/stock_account/__init__.py index dc44558835c..d84fb182e8f 100644 --- a/addons/stock_account/__init__.py +++ b/addons/stock_account/__init__.py @@ -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 diff --git a/addons/stock_account/__openerp__.py b/addons/stock_account/__openerp__.py index a48ea757b6c..8d5c0464fcd 100644 --- a/addons/stock_account/__openerp__.py +++ b/addons/stock_account/__openerp__.py @@ -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, diff --git a/addons/stock_account/product_data.xml b/addons/stock_account/data/product_data.xml similarity index 98% rename from addons/stock_account/product_data.xml rename to addons/stock_account/data/product_data.xml index 83de85fd67e..f1c3044dced 100644 --- a/addons/stock_account/product_data.xml +++ b/addons/stock_account/data/product_data.xml @@ -1,7 +1,6 @@ - + - property_stock_account_output @@ -26,6 +25,5 @@ - - + diff --git a/addons/stock_account/stock_account_data.xml b/addons/stock_account/data/stock_account_data.xml similarity index 98% rename from addons/stock_account/stock_account_data.xml rename to addons/stock_account/data/stock_account_data.xml index 1db707e2ec2..f3bffa3a6d3 100644 --- a/addons/stock_account/stock_account_data.xml +++ b/addons/stock_account/data/stock_account_data.xml @@ -1,7 +1,6 @@ - + - Cost Method Property @@ -27,6 +26,5 @@ manual_periodic selection - - \ No newline at end of file + \ No newline at end of file diff --git a/addons/stock_account/stock_account_post_install.yml b/addons/stock_account/data/stock_account_data_post_install.yml similarity index 100% rename from addons/stock_account/stock_account_post_install.yml rename to addons/stock_account/data/stock_account_data_post_install.yml diff --git a/addons/stock_account/stock_account_demo.yml b/addons/stock_account/data/stock_account_demo.yml similarity index 100% rename from addons/stock_account/stock_account_demo.yml rename to addons/stock_account/data/stock_account_demo.yml diff --git a/addons/stock_account/models/__init__.py b/addons/stock_account/models/__init__.py new file mode 100644 index 00000000000..7564d3b99c9 --- /dev/null +++ b/addons/stock_account/models/__init__.py @@ -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 diff --git a/addons/stock_account/models/account_chart_template.py b/addons/stock_account/models/account_chart_template.py new file mode 100644 index 00000000000..fdf75e5978e --- /dev/null +++ b/addons/stock_account/models/account_chart_template.py @@ -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 diff --git a/addons/stock_account/models/account_invoice.py b/addons/stock_account/models/account_invoice.py new file mode 100644 index 00000000000..b4149ead98b --- /dev/null +++ b/addons/stock_account/models/account_invoice.py @@ -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) diff --git a/addons/stock_account/models/product.py b/addons/stock_account/models/product.py new file mode 100644 index 00000000000..96d311416c1 --- /dev/null +++ b/addons/stock_account/models/product.py @@ -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.",) diff --git a/addons/stock_account/models/stock.py b/addons/stock_account/models/stock.py new file mode 100644 index 00000000000..a9c9d430837 --- /dev/null +++ b/addons/stock_account/models/stock.py @@ -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 diff --git a/addons/stock_account/models/stock_config_settings.py b/addons/stock_account/models/stock_config_settings.py new file mode 100644 index 00000000000..d782cce0615 --- /dev/null +++ b/addons/stock_account/models/stock_config_settings.py @@ -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 diff --git a/addons/stock_account/product.py b/addons/stock_account/product.py deleted file mode 100644 index 8572d670438..00000000000 --- a/addons/stock_account/product.py +++ /dev/null @@ -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.",), - } diff --git a/addons/stock_account/report/__init__.py b/addons/stock_account/report/__init__.py new file mode 100644 index 00000000000..6435c7f35ed --- /dev/null +++ b/addons/stock_account/report/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import stock_history diff --git a/addons/stock_account/report/stock_history.py b/addons/stock_account/report/stock_history.py new file mode 100644 index 00000000000..716ec6248cc --- /dev/null +++ b/addons/stock_account/report/stock_history.py @@ -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 + )""") diff --git a/addons/stock_account/wizard/stock_valuation_history_view.xml b/addons/stock_account/report/stock_history_views.xml similarity index 62% rename from addons/stock_account/wizard/stock_valuation_history_view.xml rename to addons/stock_account/report/stock_history_views.xml index 7c0955daed4..888ac577e26 100644 --- a/addons/stock_account/wizard/stock_valuation_history_view.xml +++ b/addons/stock_account/report/stock_history_views.xml @@ -1,39 +1,5 @@ - - - - - Inventory at Date - wizard.valuation.history - -
-

- Choose a date in the past to get the inventory at that date. -

-

- Retrieve the curent stock valuation. -

- - - - -
-
-
-
-
- - Inventory at Date - wizard.valuation.history - form - form - - new - - - + Stock Value At Date stock.history @@ -92,6 +58,4 @@ -
-
- + diff --git a/addons/stock_account/res_config.py b/addons/stock_account/res_config.py deleted file mode 100644 index 1a52143257c..00000000000 --- a/addons/stock_account/res_config.py +++ /dev/null @@ -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 {} diff --git a/addons/stock_account/security/stock_account_security.xml b/addons/stock_account/security/stock_account_security.xml index 7c30c4ab08d..838ffdf5fb2 100644 --- a/addons/stock_account/security/stock_account_security.xml +++ b/addons/stock_account/security/stock_account_security.xml @@ -1,5 +1,5 @@ - + @@ -18,5 +18,5 @@ - + diff --git a/addons/stock_account/stock_account.py b/addons/stock_account/stock_account.py deleted file mode 100644 index 798e8fdda5d..00000000000 --- a/addons/stock_account/stock_account.py +++ /dev/null @@ -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 diff --git a/addons/stock_account/test/stock_valuation_account.xml b/addons/stock_account/test/stock_valuation_account.xml index 4b9513fd916..40e61a96be1 100644 --- a/addons/stock_account/test/stock_valuation_account.xml +++ b/addons/stock_account/test/stock_valuation_account.xml @@ -1,5 +1,5 @@ - + @@ -18,4 +18,4 @@ - + diff --git a/addons/stock_account/product_view.xml b/addons/stock_account/views/product_views.xml similarity index 99% rename from addons/stock_account/product_view.xml rename to addons/stock_account/views/product_views.xml index 3c6b8072702..fe1ad3d92fc 100644 --- a/addons/stock_account/product_view.xml +++ b/addons/stock_account/views/product_views.xml @@ -1,5 +1,5 @@ - + @@ -99,4 +99,4 @@ - + diff --git a/addons/stock_account/stock_account_view.xml b/addons/stock_account/views/stock_account_views.xml similarity index 98% rename from addons/stock_account/stock_account_view.xml rename to addons/stock_account/views/stock_account_views.xml index d581eddcda9..bd817ab8bd3 100644 --- a/addons/stock_account/stock_account_view.xml +++ b/addons/stock_account/views/stock_account_views.xml @@ -1,5 +1,5 @@ - + @@ -39,4 +39,4 @@ - + diff --git a/addons/stock_account/res_config_view.xml b/addons/stock_account/views/stock_config_settings_views.xml similarity index 88% rename from addons/stock_account/res_config_view.xml rename to addons/stock_account/views/stock_config_settings_views.xml index c106fd30081..f2e45662a48 100644 --- a/addons/stock_account/res_config_view.xml +++ b/addons/stock_account/views/stock_config_settings_views.xml @@ -1,5 +1,5 @@ - + stock.config.settings.inherit @@ -8,11 +8,11 @@ - + - + diff --git a/addons/stock_account/wizard/__init__.py b/addons/stock_account/wizard/__init__.py index 860c206efa0..81b976cd812 100644 --- a/addons/stock_account/wizard/__init__.py +++ b/addons/stock_account/wizard/__init__.py @@ -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 diff --git a/addons/stock_account/wizard/stock_change_standard_price.py b/addons/stock_account/wizard/stock_change_standard_price.py index 5c0b1ce9197..ccda1ee7ee5 100644 --- a/addons/stock_account/wizard/stock_change_standard_price.py +++ b/addons/stock_account/wizard/stock_change_standard_price.py @@ -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'} diff --git a/addons/stock_account/wizard/stock_change_standard_price_view.xml b/addons/stock_account/wizard/stock_change_standard_price_views.xml similarity index 98% rename from addons/stock_account/wizard/stock_change_standard_price_view.xml rename to addons/stock_account/wizard/stock_change_standard_price_views.xml index ef1dd334fa1..806f6883b72 100644 --- a/addons/stock_account/wizard/stock_change_standard_price_view.xml +++ b/addons/stock_account/wizard/stock_change_standard_price_views.xml @@ -1,5 +1,5 @@ - + Change Standard Price @@ -34,5 +34,4 @@ - - + \ No newline at end of file diff --git a/addons/stock_account/wizard/stock_valuation_history.py b/addons/stock_account/wizard/stock_valuation_history.py deleted file mode 100644 index ace4a3fb0bd..00000000000 --- a/addons/stock_account/wizard/stock_valuation_history.py +++ /dev/null @@ -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 - )""") diff --git a/addons/stock_account/wizard/wizard_valuation_history.py b/addons/stock_account/wizard/wizard_valuation_history.py new file mode 100644 index 00000000000..889db533a83 --- /dev/null +++ b/addons/stock_account/wizard/wizard_valuation_history.py @@ -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, + } diff --git a/addons/stock_account/wizard/wizard_valuation_history_views.xml b/addons/stock_account/wizard/wizard_valuation_history_views.xml new file mode 100644 index 00000000000..bf65b78f0f6 --- /dev/null +++ b/addons/stock_account/wizard/wizard_valuation_history_views.xml @@ -0,0 +1,34 @@ + + + + Inventory at Date + wizard.valuation.history + +
+

+ Choose a date in the past to get the inventory at that date. +

+

+ Retrieve the curent stock valuation. +

+ + + + +
+
+
+
+
+ + Inventory at Date + wizard.valuation.history + form + form + + new + + +
diff --git a/addons/stock_calendar/__init__.py b/addons/stock_calendar/__init__.py index 7a3be6818a8..81fc81d29c9 100644 --- a/addons/stock_calendar/__init__.py +++ b/addons/stock_calendar/__init__.py @@ -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 diff --git a/addons/stock_calendar/__openerp__.py b/addons/stock_calendar/__openerp__.py index d6c763073ee..f771d78acf1 100644 --- a/addons/stock_calendar/__openerp__.py +++ b/addons/stock_calendar/__openerp__.py @@ -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, diff --git a/addons/stock_calendar/models/__init__.py b/addons/stock_calendar/models/__init__.py new file mode 100644 index 00000000000..03dad687433 --- /dev/null +++ b/addons/stock_calendar/models/__init__.py @@ -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 diff --git a/addons/stock_calendar/models/procurement.py b/addons/stock_calendar/models/procurement.py new file mode 100644 index 00000000000..9fb0cc1dd01 --- /dev/null +++ b/addons/stock_calendar/models/procurement.py @@ -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) diff --git a/addons/stock_calendar/resource.py b/addons/stock_calendar/models/resource.py similarity index 81% rename from addons/stock_calendar/resource.py rename to addons/stock_calendar/models/resource.py index a52c8e6fcc6..e0e2c22dc07 100644 --- a/addons/stock_calendar/resource.py +++ b/addons/stock_calendar/models/resource.py @@ -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'), - } diff --git a/addons/stock_calendar/models/stock.py b/addons/stock_calendar/models/stock.py new file mode 100644 index 00000000000..660bd42bc05 --- /dev/null +++ b/addons/stock_calendar/models/stock.py @@ -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) diff --git a/addons/stock_calendar/procurement.py b/addons/stock_calendar/procurement.py deleted file mode 100644 index 79b2b05871c..00000000000 --- a/addons/stock_calendar/procurement.py +++ /dev/null @@ -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) diff --git a/addons/stock_calendar/stock.py b/addons/stock_calendar/stock.py deleted file mode 100644 index 0e10235d063..00000000000 --- a/addons/stock_calendar/stock.py +++ /dev/null @@ -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 diff --git a/addons/stock_calendar/stock_calendar_view.xml b/addons/stock_calendar/stock_calendar_view.xml deleted file mode 100644 index 86d3dcd5e29..00000000000 --- a/addons/stock_calendar/stock_calendar_view.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - stock.warehouse.orderpoint.form.inherit - stock.warehouse.orderpoint - - - - - - - - - - - - resource.calendar.attendance.tree.procurement - - resource.calendar.attendance - - - - - - - - - - procurement.order.form.stock.inherit - procurement.order - - - -