From 2af100ef529d77dd22f16f71edcfb8a953581c03 Mon Sep 17 00:00:00 2001 From: Chanakya-OSI Date: Thu, 11 Jan 2024 14:02:06 +0530 Subject: [PATCH 1/2] [ADD] Added WIP modules from MIT + Adjusted according to Cab --- mrp_account_analytic/models/mrp_workorder.py | 12 +- mrp_account_analytic/models/stock_move.py | 33 +-- mrp_account_analytic_wip/__init__.py | 1 + mrp_account_analytic_wip/__manifest__.py | 2 + mrp_account_analytic_wip/models/__init__.py | 4 +- .../models/account_analytic_tracking.py | 8 +- mrp_account_analytic_wip/models/mrp_bom.py | 15 +- .../models/mrp_production.py | 188 +++++++++++++++--- .../models/mrp_workorder.py | 98 +++++++-- mrp_account_analytic_wip/models/product.py | 2 + mrp_account_analytic_wip/models/stock_move.py | 62 +++++- .../views/mrp_production_views.xml | 15 +- mrp_account_analytic_wip/wizards/__init__.py | 1 + .../wizards/mrp_confirmation.py | 10 + 14 files changed, 354 insertions(+), 97 deletions(-) create mode 100644 mrp_account_analytic_wip/wizards/__init__.py create mode 100644 mrp_account_analytic_wip/wizards/mrp_confirmation.py diff --git a/mrp_account_analytic/models/mrp_workorder.py b/mrp_account_analytic/models/mrp_workorder.py index 39828861325..614b522db6c 100644 --- a/mrp_account_analytic/models/mrp_workorder.py +++ b/mrp_account_analytic/models/mrp_workorder.py @@ -25,18 +25,18 @@ def _prepare_mrp_workorder_analytic_item(self): "amount": -self.duration / 60 * self.workcenter_id.costs_hour, } - def generate_mrp_work_analytic_line(self): + def generate_mrp_work_analytic_line(self): AnalyticLine = self.env["account.analytic.line"].sudo() for timelog in self: line_vals = timelog._prepare_mrp_workorder_analytic_item() analytic_line = AnalyticLine.create(line_vals) analytic_line.on_change_unit_amount() - @api.model_create_multi - def create(self, vals_list): - timelog = super().create(vals_list) - timelog_with_date_end = timelog.filtered("date_end") - timelog_with_date_end.generate_mrp_work_analytic_line() + @api.model + def create(self, vals): + timelog = super().create(vals) + if vals.get("date_end"): + timelog.generate_mrp_work_analytic_line() return timelog def write(self, vals): diff --git a/mrp_account_analytic/models/stock_move.py b/mrp_account_analytic/models/stock_move.py index 7f6a0e60585..37d00d72612 100644 --- a/mrp_account_analytic/models/stock_move.py +++ b/mrp_account_analytic/models/stock_move.py @@ -54,27 +54,30 @@ def write(self, vals): self.generate_mrp_raw_analytic_line() return res - @api.model_create_multi - def create(self, vals_list): - res = super().create(vals_list) - sm_has_qty_done = res.filtered("quantity_done") - sm_has_qty_done.generate_mrp_raw_analytic_line() + @api.model + def create(self, vals): + qty_done = vals.get("quantity_done") + res = super().create(vals) + if qty_done: + res.generate_mrp_raw_analytic_line() return res class StockMoveLine(models.Model): _inherit = "stock.move.line" - def write(self, vals): + # def write(self, vals): + # qty_done = vals.get("qty_done") + # for rec in self: + # if qty_done: + # rec.mapped("move_id").generate_mrp_raw_analytic_line() + # res = super().write(vals) + # return res + + @api.model + def create(self, vals): qty_done = vals.get("qty_done") - res = super().write(vals) + res = super().create(vals) if qty_done: - self.mapped("move_id").generate_mrp_raw_analytic_line() - return res - - @api.model_create_multi - def create(self, vals_list): - res = super().create(vals_list) - sml_has_qty_done = res.filtered("qty_done") - sml_has_qty_done.mapped("move_id").generate_mrp_raw_analytic_line() + res.mapped("move_id").generate_mrp_raw_analytic_line() return res diff --git a/mrp_account_analytic_wip/__init__.py b/mrp_account_analytic_wip/__init__.py index bb83730e956..426f7ea72a5 100644 --- a/mrp_account_analytic_wip/__init__.py +++ b/mrp_account_analytic_wip/__init__.py @@ -2,3 +2,4 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). from . import models +# from . import wizards diff --git a/mrp_account_analytic_wip/__manifest__.py b/mrp_account_analytic_wip/__manifest__.py index f02e1b50087..15db50e20b7 100644 --- a/mrp_account_analytic_wip/__manifest__.py +++ b/mrp_account_analytic_wip/__manifest__.py @@ -12,6 +12,8 @@ "depends": [ "mrp_account_analytic", "account_analytic_wip", + # TODO: abusive dependency, modularity should be improved + # "stock_inventory_revaluation_mrp", ], "data": [ "views/mrp_production_views.xml", diff --git a/mrp_account_analytic_wip/models/__init__.py b/mrp_account_analytic_wip/models/__init__.py index 1f6e804ab80..144b2347b12 100644 --- a/mrp_account_analytic_wip/models/__init__.py +++ b/mrp_account_analytic_wip/models/__init__.py @@ -1,9 +1,9 @@ from . import account_analytic_line from . import account_analytic_tracking -from . import mrp_bom +#from . import mrp_bom from . import mrp_production from . import mrp_workcenter from . import mrp_workorder from . import stock_move from . import stock_location -from . import product +#from . import product diff --git a/mrp_account_analytic_wip/models/account_analytic_tracking.py b/mrp_account_analytic_wip/models/account_analytic_tracking.py index 25c6421573c..1b278f661d4 100644 --- a/mrp_account_analytic_wip/models/account_analytic_tracking.py +++ b/mrp_account_analytic_wip/models/account_analytic_tracking.py @@ -115,13 +115,14 @@ def _get_unit_cost(self): "product_id.standard_price", "actual_stock_move_ids", "actual_workorder_ids", + "stock_move_id" ) def _compute_actual_amount(self): currency = self.env.company.currency_id for item in self: if item.state == "cancel" or item.child_ids: item.actual_amount = 0.0 - elif item.state == "done": + elif item.state == 'done': return elif not item.production_id: super(AnalyticTrackingItem, item)._compute_actual_amount() @@ -129,6 +130,11 @@ def _compute_actual_amount(self): # Specific Actuals calculation on MOs, using current cost # instead of the historical cost stored in Anaytic Items unit_cost = item.product_id.standard_price + if item.stock_move_id.product_id.tracking=='serial' and \ + item.stock_move_id.raw_material_production_id: + # Calculate actual cost based on real price of SN instead of standard. + unit_cost = sum(item.stock_move_id.mapped("move_line_ids").mapped("lot_id").mapped("real_price")) + workcenter = item.workcenter_id or item.workorder_id.workcenter_id items = item | item.parent_id raw_qty = sum(items.actual_stock_move_ids.mapped("quantity_done")) ops_qty = sum(items.actual_workorder_ids.mapped("duration")) / 60 diff --git a/mrp_account_analytic_wip/models/mrp_bom.py b/mrp_account_analytic_wip/models/mrp_bom.py index a08d52ed469..7cbf090bab0 100644 --- a/mrp_account_analytic_wip/models/mrp_bom.py +++ b/mrp_account_analytic_wip/models/mrp_bom.py @@ -1,6 +1,7 @@ # Copyright (C) 2023 Open Source Integrators # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +## this can be deleted. not needed with FIFO from odoo import api, fields, models @@ -14,8 +15,8 @@ def _prepare_raw_tracking_item_values(self, product_uom_qty): self.ensure_one() product = self.product_tmpl_id or self.product_id bom_qty = self.product_uom_id._compute_quantity( - self.product_qty, product.uom_id - ) + self.product_qty, product.uom_id + ) factor = product_uom_qty / bom_qty # Each distinct Product will be one Tracking Item # So multiple BOM lines for the same Product need to be aggregated @@ -25,8 +26,7 @@ def _prepare_raw_tracking_item_values(self, product_uom_qty): "product_id": product.id, "planned_qty": sum( x.product_qty for x in lines if x.product_id == product - ) - * factor, + ) * factor, } for product in lines.product_id ] @@ -37,8 +37,8 @@ def _prepare_ops_tracking_item_values(self, product_uom_qty): # So multiple BOM lines for the same Work Center need to be aggregated product = self.product_tmpl_id or self.product_id bom_qty = self.product_uom_id._compute_quantity( - self.product_qty, product.uom_id - ) + self.product_qty, product.uom_id + ) factor = product_uom_qty / bom_qty lines = self.operation_ids return [ @@ -47,8 +47,7 @@ def _prepare_ops_tracking_item_values(self, product_uom_qty): "workcenter_id": workcenter.id, "planned_qty": sum( x.time_cycle for x in lines if x.workcenter_id == workcenter - ) - * factor + ) * factor / 60, } for workcenter in lines.workcenter_id diff --git a/mrp_account_analytic_wip/models/mrp_production.py b/mrp_account_analytic_wip/models/mrp_production.py index 034da345791..24cc0a78603 100644 --- a/mrp_account_analytic_wip/models/mrp_production.py +++ b/mrp_account_analytic_wip/models/mrp_production.py @@ -4,7 +4,8 @@ import logging from odoo import _, api, exceptions, fields, models -from odoo.tools import float_round +from odoo.tools import float_is_zero, float_round +from odoo.exceptions import UserError _logger = logging.getLogger(__name__) @@ -30,7 +31,7 @@ class MRPProduction(models.Model): ) currency_id = fields.Many2one("res.currency", related="company_id.currency_id") - is_post_wip_automatic = fields.Boolean(default=True) + is_post_wip_automatic = fields.Boolean(default=False) @api.depends( "move_raw_ids.state", @@ -66,13 +67,14 @@ def _get_tracking_items(self): Returns a recordset with the related Ttacking Items """ return ( - self.bom_analytic_tracking_item_ids - | self.bom_analytic_tracking_item_ids.child_ids + self.mapped("move_raw_ids.analytic_tracking_item_id") + | self.mapped("workorder_ids.analytic_tracking_item_id") + | self.mapped("workorder_ids.analytic_tracking_item_id.child_ids") ) @api.depends( - "bom_analytic_tracking_item_ids", - "bom_analytic_tracking_item_ids.child_ids", + "move_raw_ids.analytic_tracking_item_id", + "workorder_ids.analytic_tracking_item_id", ) def _compute_analytic_tracking_item(self): for mo in self: @@ -165,8 +167,9 @@ def action_post_inventory_wip(self, cancel_backorder=False): """ for order in self: moves_all = order.move_raw_ids - for move in moves_all.filtered(lambda m: m.quantity_done): - move.product_uom_qty = move.quantity_done + # comment code as if move having more then 1 qty in then if update the cousume qty it change the The cousume Qty + # for move in moves_all.filtered(lambda m: m.quantity_done): + # move.product_uom_qty = move.quantity_done # Raw Material Consumption, closely following _post_inventory() moves_not_to_do = order.move_raw_ids.filtered(lambda x: x.state == "done") moves_to_do = order.move_raw_ids.filtered( @@ -211,7 +214,7 @@ def _prepare_clear_wip_account_line(self, account, amount): # "analytic_account_id": self.analytic_account_id.id, } - def clear_wip_final_old(self): + def clear_wip_final(self): """ Add final Clear WIP JE journal entry. Looks up the WIP account balance and clears it using the Variance account. @@ -283,7 +286,7 @@ def _prepare_clear_wip_account_move_line(self, product, account, amount): "credit": -amount if amount < 0.0 else 0.0, } - def clear_wip_final(self): + def clear_wip_final_boak(self): """ Add final Clear WIP JE journal entry using tracked items. Looks up the WIP account balance and clears it using the Variance account. @@ -301,9 +304,7 @@ def clear_wip_final(self): move_lines.extend( [ prod._prepare_clear_wip_account_move_line( - product, - acc_wip_prod, - prod.product_uom_qty * product.standard_price, + product, acc_wip_prod, prod.product_uom_qty * product.standard_price ) ] ) @@ -323,7 +324,7 @@ def clear_wip_final(self): prod._prepare_clear_wip_account_move_line( item.product_id, accounts["stock_wip"], - -round(item.actual_amount, 2), + -round(item.actual_amount,2), ) ] ) @@ -334,7 +335,7 @@ def clear_wip_final(self): prod._prepare_clear_wip_account_move_line( item.product_id, accounts["stock_variance"], - round(item.difference_actual_amount, 2), + round(item.difference_actual_amount,2) ) ] ) @@ -348,7 +349,7 @@ def clear_wip_final(self): prod._prepare_clear_wip_account_move_line( item.product_id, accounts["stock_wip"], - -round(item.actual_amount, 2), + -round(item.actual_amount,2), ) ] ) @@ -387,15 +388,15 @@ def clear_wip_final(self): debit = 0.0 credit = 0.0 for line in move_lines: - debit += line["debit"] - credit += line["credit"] + debit += line['debit'] + credit += line['credit'] if credit - debit: move_lines.extend( [ prod._prepare_clear_wip_account_move_line( prod.product_id, accounts["stock_variance"], - -round(debit - credit, 2), + -round(debit-credit, 2), ) ] ) @@ -433,7 +434,8 @@ def action_confirm(self): just after MO confirmation. """ res = super().action_confirm() - self.populate_ref_bom_tracking_items() + self.mapped("move_raw_ids").populate_tracking_items(set_planned=True) + self.mapped("workorder_ids").populate_tracking_items(set_planned=True) return res def _get_matching_tracking_item(self, vals, new_tracking_items=None): @@ -530,12 +532,8 @@ def populate_ref_bom_tracking_items(self): reference_bom = production.product_id.cost_reference_bom_id if not reference_bom: continue - ref_raw_vals = reference_bom._prepare_raw_tracking_item_values( - production.product_uom_qty - ) - ref_ops_vals = reference_bom._prepare_ops_tracking_item_values( - production.product_uom_qty - ) + ref_raw_vals = reference_bom._prepare_raw_tracking_item_values(production.product_uom_qty) + ref_ops_vals = reference_bom._prepare_ops_tracking_item_values(production.product_uom_qty) ref_items = production._populate_ref_bom_tracking_items( ref_raw_vals + ref_ops_vals ) @@ -577,7 +575,70 @@ def button_mark_done(self): # tracking.clear_wip_journal_entries() # Raw Material - clear final WIP and post Variances mfg_done.clear_wip_final() + + # Below code will fix the FIFO SN costing for Raw material, FG and By product + #NOT NEEDED FOR CABINTOUCH + # if self.product_id.cost_method =='fifo': + # # recalculate all JE for last MO + # finished_move = self.move_finished_ids.filtered( + # lambda x: x.product_id == self.product_id and x.state == 'done' and x.quantity_done > 0) + # consumed_moves = self.move_raw_ids + # if finished_move: + # work_center_cost = 0 + # finished_move.ensure_one() + # fg_svl_ids = finished_move.sudo().stock_valuation_layer_ids + # for work_order in self.workorder_ids: + # time_lines = work_order.time_ids.filtered(lambda t: t.date_end and not t.cost_already_recorded) + # work_center_cost += work_order._cal_cost(times=time_lines) + # time_lines.write({'cost_already_recorded': True}) + # qty_done = finished_move.product_uom._compute_quantity( + # finished_move.quantity_done, finished_move.product_id.uom_id) + # extra_cost = self.extra_cost * qty_done + # total_cost = - sum(consumed_moves.sudo().stock_valuation_layer_ids.mapped('value')) + work_center_cost + extra_cost + # byproduct_moves = self.move_byproduct_ids.filtered(lambda m: m.state == 'done' and m.quantity_done > 0) + # byproduct_cost_share = 0 + # for byproduct in byproduct_moves: + # if byproduct.cost_share == 0: + # continue + # byproduct_cost_share += byproduct.cost_share + # if byproduct.product_id.cost_method in ('fifo', 'average'): + # byproduct.price_unit = total_cost * byproduct.cost_share / 100 / byproduct.product_uom._compute_quantity(byproduct.quantity_done, byproduct.product_id.uom_id) + # by_product_svl = byproduct.sudo().stock_valuation_layer_ids + # self._correct_svl_je(by_product_svl, byproduct, byproduct.price_unit) + # if finished_move.product_id.cost_method in ('fifo', 'average'): + # finished_move.price_unit = total_cost * float_round(1 - byproduct_cost_share / 100, precision_rounding=0.0001) / qty_done + # total_cost = finished_move.price_unit + # self.lot_producing_id.real_price = total_cost + # fg_svl = finished_move.stock_valuation_layer_ids and finished_move.stock_valuation_layer_ids[0] or [] + # self._correct_svl_je(fg_svl, finished_move, total_cost) + if self.analytic_account_id: + self.analytic_account_id.line_ids.write({'manufacturing_order_id':self.id}) return res + + # NOT NEEDED FOR CABIN TOUCH + # def _correct_svl_je(self, svl, stock_move, total_cost): + # account_move_id = svl.account_move_id + # svl.unit_cost = total_cost / (svl.quantity if svl.quantity>0 else 1) + # svl.value = svl.unit_cost * svl.quantity + # svl.remaining_value = svl.unit_cost * svl.quantity + # + # if not account_move_id: + # svl._validate_accounting_entries() + # else: + # # Change the SVl with correct cost + # account_move_id.button_draft() + # # The Valuation Layer has been changed, + # # now we have to edit the STJ Entry + # for ji_id in account_move_id.line_ids: + # if ji_id.credit != 0: + # ji_id.with_context(check_move_validity=False).write( + # {"credit": total_cost} + # ) + # else: + # ji_id.with_context(check_move_validity=False).write( + # {"debit": total_cost} + # ) + # account_move_id.action_post() def action_cancel(self): res = super().action_cancel() @@ -611,11 +672,82 @@ def write(self, vals): ) if "analytic_account_id" in vals or is_workcenter_change: + # From BOAK Code + # confirmed_mos = self.filtered(lambda x: x.state == "confirmed") + # confirmed_mos.populate_ref_bom_tracking_items() confirmed_mos = self.filtered(lambda x: x.state == "confirmed") - confirmed_mos.populate_ref_bom_tracking_items() + confirmed_mos.move_raw_ids.populate_tracking_items() + confirmed_mos.workorder_ids.populate_tracking_items() return True def copy_data(self, default=None): default = dict(default or {}) - default["bom_analytic_tracking_item_ids"] = False + default['bom_analytic_tracking_item_ids'] = False return super(MRPProduction, self).copy_data(default=default) + + def _create_workorder(self): + res = super()._create_workorder() + for production in self: + for workorder in production.workorder_ids: + workorder.duration_planned = workorder.duration_expected + return res + + def _check_sn_uniqueness(self): + """ Alert the user if the serial number as already been consumed/produced + WIP Module is also creating other JE with Virtual / Production Location. + We need to bypass the check for Current Production, there can be multiple moves with Virtual Production for same SN in WIP module. + """ + if self.product_tracking == 'serial' and self.lot_producing_id: + if self._is_finished_sn_already_produced(self.lot_producing_id): + raise UserError(_('This serial number for product %s has already been produced', self.product_id.name)) + + for move in self.move_finished_ids: + if move.has_tracking != 'serial' or move.product_id == self.product_id: + continue + for move_line in move.move_line_ids: + if self._is_finished_sn_already_produced(move_line.lot_id, excluded_sml=move_line): + raise UserError(_('The serial number %(number)s used for byproduct %(product_name)s has already been produced', + number=move_line.lot_id.name, product_name=move_line.product_id.name)) + + for move in self.move_raw_ids: + if move.has_tracking != 'serial': + continue + for move_line in move.move_line_ids: + if float_is_zero(move_line.qty_done, precision_rounding=move_line.product_uom_id.rounding): + continue + message = _('The serial number %(number)s used for component %(component)s has already been consumed', + number=move_line.lot_id.name, + component=move_line.product_id.name) + co_prod_move_lines = self.move_raw_ids.move_line_ids + + # Check presence of same sn in previous productions + duplicates = self.env['stock.move.line'].search_count([ + ('lot_id', '=', move_line.lot_id.id), + ('qty_done', '=', 1), + ('state', '=', 'done'), + ('location_dest_id.usage', '=', 'production'), + ('production_id', '!=', False), + ('production_id', '!=',self.id) #In this core odoo method only this change has been added. + ]) + if duplicates: + # Maybe some move lines have been compensated by unbuild + duplicates_returned = move.product_id._count_returned_sn_products(move_line.lot_id) + removed = self.env['stock.move.line'].search_count([ + ('lot_id', '=', move_line.lot_id.id), + ('state', '=', 'done'), + ('location_dest_id.scrap_location', '=', True) + ]) + unremoved = self.env['stock.move.line'].search_count([ + ('lot_id', '=', move_line.lot_id.id), + ('state', '=', 'done'), + ('location_id.scrap_location', '=', True), + ('location_dest_id.scrap_location', '=', False), + ]) + # Either removed or unbuild + if not ((duplicates_returned or removed) and duplicates - duplicates_returned - removed + unremoved == 0): + raise UserError(message) + # Check presence of same sn in current production + duplicates = co_prod_move_lines.filtered(lambda ml: ml.qty_done and ml.lot_id == move_line.lot_id) - move_line + if duplicates: + raise UserError(message) + diff --git a/mrp_account_analytic_wip/models/mrp_workorder.py b/mrp_account_analytic_wip/models/mrp_workorder.py index 9c42d036cca..5fc39bc7c1b 100644 --- a/mrp_account_analytic_wip/models/mrp_workorder.py +++ b/mrp_account_analytic_wip/models/mrp_workorder.py @@ -12,18 +12,66 @@ class MRPWorkOrder(models.Model): analytic_tracking_item_id = fields.Many2one( "account.analytic.tracking.item", string="Tracking Item", copy=False ) + # Operations added after MO confirmation have expected qty zero + duration_expected = fields.Float(default=0.0) # Make MO lock status available for views is_locked = fields.Boolean(related="production_id.is_locked") duration_planned = fields.Float(string="Planned Duration") - @api.model_create_multi - def create(self, vals_list): - new_workorders = super().create(vals_list) - new_workorders.production_id.populate_ref_bom_tracking_items() - return new_workorders + # From BOAK Code + # @api.model_create_multi + #def create(self, vals_list): + # new_workorders = super().create(vals_list) + # new_workorders.production_id.populate_ref_bom_tracking_items() + # return new_workorders # FIXME: manual time entry on Wokr Order does not generate analytic items! + def _prepare_tracking_item_values(self): + analytic = self.production_id.analytic_account_id + planned_qty = self.duration_planned / 60 + return analytic and { + "analytic_id": analytic.id, + "product_id": self.workcenter_id.analytic_product_id.id, + "workorder_id": self.id, + "planned_qty": planned_qty, + "production_id" : self.production_id.id + } + + def populate_tracking_items(self, set_planned=False): + """ + When creating a Work Order link it to a Tracking Item. + It may be an existing Tracking Item, + or a new one my be created if it doesn't exist yet. + """ + TrackingItem = self.env["account.analytic.tracking.item"] + to_populate = self.filtered( + lambda x: x.production_id.analytic_account_id + and x.production_id.state not in ("draft", "done", "cancel") + ) + all_tracking = to_populate.production_id.analytic_tracking_item_ids + for item in to_populate: + tracking = all_tracking.filtered(lambda x: x.workorder_id == self)[:1] + vals = item._prepare_tracking_item_values() + not set_planned and vals.pop("planned_qty") + if tracking: + tracking.write(vals) + else: + tracking = TrackingItem.create(vals) + item.analytic_tracking_item_id = tracking + + @api.model_create_multi + def create(self, vals): + new_workorder = super().create(vals) + new_workorder.populate_tracking_items() + return new_workorder + + + # def write(self, vals): + # res = super().write(vals) + # for timelog in self.time_ids: + # timelog.generate_mrp_work_analytic_line() + # return res class MrpWorkcenterProductivity(models.Model): _inherit = "mrp.workcenter.productivity" @@ -31,6 +79,7 @@ class MrpWorkcenterProductivity(models.Model): def _prepare_mrp_workorder_analytic_item(self): values = super()._prepare_mrp_workorder_analytic_item() # Ensure the related Tracking Item is populated + workorder = self.workorder_id if not workorder.analytic_tracking_item_id: item_vals = { @@ -44,20 +93,25 @@ def _prepare_mrp_workorder_analytic_item(self): values["product_id"] = workorder.workcenter_id.analytic_product_id.id return values - -class MrpWorkcenterProductivityLoss(models.Model): - _inherit = "mrp.workcenter.productivity.loss" - - def _convert_to_duration(self, date_start, date_stop, workcenter=False): - """Convert a date range into a duration in minutes. - If the productivity type is not from an employee (extra hours are allow) - and the workcenter has a calendar, convert the dates into a duration based on - working hours. - """ - duration = super()._convert_to_duration(date_start, date_stop, workcenter) - if workcenter and workcenter.resource_calendar_id: - r = workcenter._get_work_days_data_batch(date_start, date_stop)[ - workcenter.id - ]["hours"] - duration = r * 60 - return duration + def generate_mrp_work_analytic_line(self): + res = super().generate_mrp_work_analytic_line() + # When recording actuals, consider posting WIp immedately + mos_to_post = self.production_id.filtered("is_post_wip_automatic") + mos_to_post.action_post_inventory_wip() + return res + +# class MrpWorkcenterProductivityLoss(models.Model): +# _inherit = "mrp.workcenter.productivity.loss" +# +# def _convert_to_duration(self, date_start, date_stop, workcenter=False): +# """ Convert a date range into a duration in minutes. +# If the productivity type is not from an employee (extra hours are allow) +# and the workcenter has a calendar, convert the dates into a duration based on +# working hours. +# """ +# duration = super()._convert_to_duration(date_start, date_stop, workcenter) +# for productivity_loss in self: +# if workcenter and workcenter.resource_calendar_id: +# r = workcenter._get_work_days_data_batch(date_start, date_stop)[workcenter.id]['hours'] +# duration = r * 60 +# return duration diff --git a/mrp_account_analytic_wip/models/product.py b/mrp_account_analytic_wip/models/product.py index 01a51e8064f..bffebf1f8ff 100644 --- a/mrp_account_analytic_wip/models/product.py +++ b/mrp_account_analytic_wip/models/product.py @@ -1,5 +1,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. +## this can be deleted. not needed with FIFO + from odoo import fields, models diff --git a/mrp_account_analytic_wip/models/stock_move.py b/mrp_account_analytic_wip/models/stock_move.py index 7657257b999..c8397d19b36 100644 --- a/mrp_account_analytic_wip/models/stock_move.py +++ b/mrp_account_analytic_wip/models/stock_move.py @@ -80,20 +80,74 @@ def _prepare_mrp_raw_material_analytic_line(self): values["analytic_tracking_item_id"] = self.analytic_tracking_item_id.id return values + # From Boak Code def generate_mrp_raw_analytic_line(self): res = super().generate_mrp_raw_analytic_line() # When recording actuals, consider posting WIP immediately mos_to_post = self.raw_material_production_id.filtered("is_post_wip_automatic") mos_to_post.action_post_inventory_wip() return res + # End From Boak Code + + def _prepare_tracking_item_values(self): + analytic = self.raw_material_production_id.analytic_account_id + planned_qty = self.qty_planned + return { + "analytic_id": analytic.id, + "product_id": self.product_id.id, + "stock_move_id": self.id, + "planned_qty": self.product_uom_qty, + "production_id" : self.raw_material_production_id.id + } + + def populate_tracking_items(self, set_planned=False): + """ + When creating an Analytic Item, + link it to a Tracking Item, the may have to be created if it doesn't exist. + """ + TrackingItem = self.env["account.analytic.tracking.item"] + to_populate = self.filtered( + lambda x: x.raw_material_production_id.analytic_account_id + and x.raw_material_production_id.state not in ("draft", "done", "cancel") + ) + all_tracking = to_populate.raw_material_production_id.analytic_tracking_item_ids + for item in to_populate: + tracking = all_tracking.filtered( + lambda x: x.stock_move_id and x.product_id == item.product_id + )[:1] + vals = item._prepare_tracking_item_values() + not set_planned and vals.pop("planned_qty") + if self._context.get("from_create"): + tracking = TrackingItem.create(vals) + else: + if tracking: + tracking.write(vals) + else: + tracking = TrackingItem.create(vals) + item.analytic_tracking_item_id = tracking @api.model def create(self, vals): new_moves = super().create(vals) - new_moves.raw_material_production_id.populate_ref_bom_tracking_items() + # From BOAK Code + # new_moves.raw_material_production_id.populate_ref_bom_tracking_items() + new_moves.with_context(from_create=True).populate_tracking_items() return new_moves - + def write(self, vals): res = super().write(vals) - self.raw_material_production_id.populate_ref_bom_tracking_items() - return res + if self.raw_material_production_id.analytic_tracking_item_ids: + self.raw_material_production_id.analytic_tracking_item_ids._compute_actual_amount() + # From Boak Code + # self.raw_material_production_id.populate_ref_bom_tracking_items() + # return res + + if not self.env.context.get("flag_write_tracking"): + moves = self.filtered( + lambda x: x.raw_material_production_id.analytic_account_id + and not x.analytic_tracking_item_id + ) + moves and moves.with_context( + flag_write_tracking=True + ).populate_tracking_items() + return res \ No newline at end of file diff --git a/mrp_account_analytic_wip/views/mrp_production_views.xml b/mrp_account_analytic_wip/views/mrp_production_views.xml index 8e6a36f9a5b..a5015677722 100644 --- a/mrp_account_analytic_wip/views/mrp_production_views.xml +++ b/mrp_account_analytic_wip/views/mrp_production_views.xml @@ -4,12 +4,12 @@ mrp.production -
@@ -53,15 +53,8 @@ - - + + diff --git a/mrp_account_analytic_wip/wizards/__init__.py b/mrp_account_analytic_wip/wizards/__init__.py new file mode 100644 index 00000000000..e779b25cdca --- /dev/null +++ b/mrp_account_analytic_wip/wizards/__init__.py @@ -0,0 +1 @@ +from . import mrp_confirmation diff --git a/mrp_account_analytic_wip/wizards/mrp_confirmation.py b/mrp_account_analytic_wip/wizards/mrp_confirmation.py new file mode 100644 index 00000000000..c5b3fba0d6d --- /dev/null +++ b/mrp_account_analytic_wip/wizards/mrp_confirmation.py @@ -0,0 +1,10 @@ +from odoo import models + +class MrpConfirmation(models.TransientModel): + _inherit = "mrp.confirmation" + + def do_confirm(self): + for record in self: + if record.working_duration == 0.0: + record.date_end = record.date_start + super().do_confirm() From c0e4d7c2192f5b19975c3a56f6e1f6a516157a46 Mon Sep 17 00:00:00 2001 From: Raphael Lee Date: Tue, 16 Jan 2024 04:09:12 +0000 Subject: [PATCH 2/2] [FIX] WIP accounting fixes for CAB --- mrp_account_analytic/models/mrp_workorder.py | 15 +++++----- mrp_account_analytic/models/stock_move.py | 24 +++++++-------- .../models/mrp_production.py | 14 +++++---- mrp_account_analytic_wip/models/stock_move.py | 29 +++++++++---------- .../views/mrp_production_views.xml | 2 +- 5 files changed, 44 insertions(+), 40 deletions(-) diff --git a/mrp_account_analytic/models/mrp_workorder.py b/mrp_account_analytic/models/mrp_workorder.py index 614b522db6c..7dbe4989132 100644 --- a/mrp_account_analytic/models/mrp_workorder.py +++ b/mrp_account_analytic/models/mrp_workorder.py @@ -25,19 +25,20 @@ def _prepare_mrp_workorder_analytic_item(self): "amount": -self.duration / 60 * self.workcenter_id.costs_hour, } - def generate_mrp_work_analytic_line(self): + def generate_mrp_work_analytic_line(self): AnalyticLine = self.env["account.analytic.line"].sudo() for timelog in self: line_vals = timelog._prepare_mrp_workorder_analytic_item() analytic_line = AnalyticLine.create(line_vals) analytic_line.on_change_unit_amount() - @api.model - def create(self, vals): - timelog = super().create(vals) - if vals.get("date_end"): - timelog.generate_mrp_work_analytic_line() - return timelog + @api.model_create_multi + def create(self, vals_list): + timelogs = super().create(vals_list) + for i, timelog in enumerate(timelogs): + if vals_list[i].get("date_end"): + timelog.generate_mrp_work_analytic_line() + return timelogs def write(self, vals): res = super().write(vals) diff --git a/mrp_account_analytic/models/stock_move.py b/mrp_account_analytic/models/stock_move.py index 37d00d72612..7f4f1243820 100644 --- a/mrp_account_analytic/models/stock_move.py +++ b/mrp_account_analytic/models/stock_move.py @@ -54,12 +54,12 @@ def write(self, vals): self.generate_mrp_raw_analytic_line() return res - @api.model - def create(self, vals): - qty_done = vals.get("quantity_done") - res = super().create(vals) - if qty_done: - res.generate_mrp_raw_analytic_line() + @api.model_create_multi + def create(self, vals_list): + res = super().create(vals_list) + for i, move in enumerate(res): + if vals_list[i].get("quantity_done"): + move.generate_mrp_raw_analytic_line() return res @@ -74,10 +74,10 @@ class StockMoveLine(models.Model): # res = super().write(vals) # return res - @api.model - def create(self, vals): - qty_done = vals.get("qty_done") - res = super().create(vals) - if qty_done: - res.mapped("move_id").generate_mrp_raw_analytic_line() + @api.model_create_multi + def create(self, vals_list): + res = super().create(vals_list) + for i, line in enumerate(res): + if vals_list[i].get("qty_done"): + line.mapped("move_id").generate_mrp_raw_analytic_line() return res diff --git a/mrp_account_analytic_wip/models/mrp_production.py b/mrp_account_analytic_wip/models/mrp_production.py index 24cc0a78603..481e5a74684 100644 --- a/mrp_account_analytic_wip/models/mrp_production.py +++ b/mrp_account_analytic_wip/models/mrp_production.py @@ -31,7 +31,7 @@ class MRPProduction(models.Model): ) currency_id = fields.Many2one("res.currency", related="company_id.currency_id") - is_post_wip_automatic = fields.Boolean(default=False) + is_post_wip_automatic = fields.Boolean(default=True) @api.depends( "move_raw_ids.state", @@ -106,7 +106,11 @@ def _cal_price(self, consumed_moves): ) extra_cost = self.extra_cost * qty_done total_cost = ( - -sum(consumed_moves.sudo().stock_valuation_layer_ids.mapped("value")) + -sum( + consumed_moves.sudo() + .stock_valuation_layer_ids.filtered(lambda svl: svl.quantity < 0) + .mapped("value") + ) + work_center_cost + extra_cost ) @@ -577,7 +581,7 @@ def button_mark_done(self): mfg_done.clear_wip_final() # Below code will fix the FIFO SN costing for Raw material, FG and By product - #NOT NEEDED FOR CABINTOUCH + #NOT NEEDED FOR CABINOTCH # if self.product_id.cost_method =='fifo': # # recalculate all JE for last MO # finished_move = self.move_finished_ids.filtered( @@ -615,7 +619,7 @@ def button_mark_done(self): self.analytic_account_id.line_ids.write({'manufacturing_order_id':self.id}) return res - # NOT NEEDED FOR CABIN TOUCH + # NOT NEEDED FOR CABINOTCH # def _correct_svl_je(self, svl, stock_move, total_cost): # account_move_id = svl.account_move_id # svl.unit_cost = total_cost / (svl.quantity if svl.quantity>0 else 1) @@ -682,7 +686,7 @@ def write(self, vals): def copy_data(self, default=None): default = dict(default or {}) - default['bom_analytic_tracking_item_ids'] = False + default["bom_analytic_tracking_item_ids"] = False return super(MRPProduction, self).copy_data(default=default) def _create_workorder(self): diff --git a/mrp_account_analytic_wip/models/stock_move.py b/mrp_account_analytic_wip/models/stock_move.py index c8397d19b36..04f685ad63d 100644 --- a/mrp_account_analytic_wip/models/stock_move.py +++ b/mrp_account_analytic_wip/models/stock_move.py @@ -10,6 +10,15 @@ class StockMove(models.Model): qty_planned = fields.Float() + # Store related Tracking Item, for computation efficiency + analytic_tracking_item_id = fields.Many2one( + "account.analytic.tracking.item", + string="Tracking Item", + copy=True + # Copy Tracking item, so that when a move is split, + # it still related to the same Tracking Item + ) + # Improve the unconsume descrition on SVL and JE # (originally "Correction of False (modification of past move)") # and add link to the MO Tracking Items @@ -56,15 +65,6 @@ def _compute_should_consume_qty(self): move.should_consume_qty = 0 return res - # Store related Tracking Item, for computation efficiency - analytic_tracking_item_id = fields.Many2one( - "account.analytic.tracking.item", - string="Tracking Item", - copy=True - # Copy Tracking item, so that when a move is split, - # it still related to the same Tracking Item - ) - def _prepare_mrp_raw_material_analytic_line(self): values = super()._prepare_mrp_raw_material_analytic_line() # Ensure the related Tracking Item is populated @@ -80,14 +80,13 @@ def _prepare_mrp_raw_material_analytic_line(self): values["analytic_tracking_item_id"] = self.analytic_tracking_item_id.id return values - # From Boak Code def generate_mrp_raw_analytic_line(self): + # From Boak Code res = super().generate_mrp_raw_analytic_line() # When recording actuals, consider posting WIP immediately mos_to_post = self.raw_material_production_id.filtered("is_post_wip_automatic") mos_to_post.action_post_inventory_wip() return res - # End From Boak Code def _prepare_tracking_item_values(self): analytic = self.raw_material_production_id.analytic_account_id @@ -126,9 +125,9 @@ def populate_tracking_items(self, set_planned=False): tracking = TrackingItem.create(vals) item.analytic_tracking_item_id = tracking - @api.model - def create(self, vals): - new_moves = super().create(vals) + @api.model_create_multi + def create(self, vals_list): + new_moves = super().create(vals_list) # From BOAK Code # new_moves.raw_material_production_id.populate_ref_bom_tracking_items() new_moves.with_context(from_create=True).populate_tracking_items() @@ -150,4 +149,4 @@ def write(self, vals): moves and moves.with_context( flag_write_tracking=True ).populate_tracking_items() - return res \ No newline at end of file + return res diff --git a/mrp_account_analytic_wip/views/mrp_production_views.xml b/mrp_account_analytic_wip/views/mrp_production_views.xml index a5015677722..eb4ab1c2b35 100644 --- a/mrp_account_analytic_wip/views/mrp_production_views.xml +++ b/mrp_account_analytic_wip/views/mrp_production_views.xml @@ -9,7 +9,7 @@ name="action_post_inventory_wip" type="object" string="Post WIP" - attrs="{'invisible': [('state', 'in', ('draft', 'confirmed', 'done', 'to_close','cancel'))]}" + attrs="{'invisible': [('state', 'in', ('draft', 'confirmed', 'progress', 'done', 'to_close','cancel'))]}" />