From 420dadbd3e5847d5f3cfacf1757848dba303fa6a Mon Sep 17 00:00:00 2001 From: Johan De Taeye Date: Fri, 3 Nov 2023 16:58:19 +0100 Subject: [PATCH 1/9] improved handling of multidb odoo configurations --- frepple/controllers/frepplexml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frepple/controllers/frepplexml.py b/frepple/controllers/frepplexml.py index 4a56f8c7..35d3ac90 100644 --- a/frepple/controllers/frepplexml.py +++ b/frepple/controllers/frepplexml.py @@ -119,7 +119,7 @@ def xml(self, **kwargs): database = odoo.http.db_monodb(httprequest=req.httprequest) company_name = kwargs.get("company", req.httprequest.form.get("company", None)) company = None - if company_name: + if company_name and req.env: for i in req.env["res.company"].search( [("name", "=", company_name)], limit=1 ): From 12915242b4e11a52e4e041f10d4dd92631820014 Mon Sep 17 00:00:00 2001 From: Hicham Lahlou Date: Sun, 5 Nov 2023 05:53:17 +0100 Subject: [PATCH 2/9] handle case where the manufacturing components in Odoo contain multiple times the same item --- frepple/controllers/outbound.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/frepple/controllers/outbound.py b/frepple/controllers/outbound.py index 82fcc318..5d963b09 100644 --- a/frepple/controllers/outbound.py +++ b/frepple/controllers/outbound.py @@ -2159,6 +2159,8 @@ def export_manufacturingorders(self): quoteattr(location), quoteattr(item["name"]), ) + # dictionary needed as BOM in Odoo might have multiple lines with the same product + operation_materials = {} for mv in mv_list: consumed_item = ( self.product_product[mv["product_id"][0]] @@ -2181,10 +2183,16 @@ def export_manufacturingorders(self): self.product_product[mv["product_id"][0]]["template"], ) if qty_flow > 0: - yield '\n' % ( - -qty_flow / qty, - quoteattr(consumed_item["name"]), + operation_materials[ + consumed_item["name"] + ] = operation_materials.get(consumed_item["name"], 0) + ( + -qty_flow / qty ) + for key in operation_materials: + yield '\n' % ( + operation_materials[key], + quoteattr(key), + ) yield '\n' % ( quoteattr(item["name"]), ) @@ -2228,6 +2236,8 @@ def export_manufacturingorders(self): quoteattr(location), ) idx += 10 + # dictionary needed as BOM in Odoo might have multiple lines with the same product + operation_materials = {} for mv in mv_list: item = ( self.product_product[mv["product_id"][0]] @@ -2263,9 +2273,13 @@ def export_manufacturingorders(self): mv["product_uom"], self.product_product[mv["product_id"][0]]["template"], ) + operation_materials[item["name"]] = operation_materials.get( + item["name"], 0 + ) + (-qty_flow / qty) + for key in operation_materials: yield '\n' % ( - -qty_flow / qty, - quoteattr(item["name"]), + operation_materials[key], + quoteattr(key), ) yield "" if ( From 7f9b7bc09a8383b268de965e12065154f9878680 Mon Sep 17 00:00:00 2001 From: Hicham Lahlou Date: Tue, 21 Nov 2023 21:25:05 +0100 Subject: [PATCH 3/9] capture sales orders from individual --- frepple/controllers/outbound.py | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/frepple/controllers/outbound.py b/frepple/controllers/outbound.py index 5d963b09..c13fdf43 100644 --- a/frepple/controllers/outbound.py +++ b/frepple/controllers/outbound.py @@ -691,16 +691,22 @@ def export_customers(self): first = True for i in self.generator.getData( "res.partner", - search=[("is_company", "=", True)], - fields=["name"], + search=[], + fields=["name", "parent_id"], + order="id", ): if first: yield "\n" yield "\n" first = False - name = "%s %s" % (i["name"], i["id"]) - yield "\n" % quoteattr(name) - self.map_customers[i["id"]] = name + if not i["parent_id"] or i["id"] == i["parent_id"][0]: + name = "%s %s" % (i["name"], i["id"]) + yield "\n" % quoteattr(name) + self.map_customers[i["id"]] = ( + name + if not i["parent_id"] or i["id"] == i["parent_id"][0] + else self.map_customers[i["parent_id"][0]] + ) if not first: yield "\n" @@ -1628,19 +1634,7 @@ def getReservedQuantity(stock_move_id): j = so[i["order_id"][0]] location = j["warehouse_id"][1] customer = self.map_customers.get(j["partner_id"][0], None) - if not customer: - # The customer may be an individual. - # We check whether his/her company is in the map. - for c in self.generator.getData( - "res.partner", - ids=[j["partner_id"][0]], - fields=["commercial_partner_id"], - ): - customer = self.map_customers.get( - c["commercial_partner_id"][0], None - ) - if customer: - break + if not customer or not location or not product: # Not interested in this sales order... continue From 44e413a63957d802d0b511c05c690e1994aa87fb Mon Sep 17 00:00:00 2001 From: Hicham Lahlou Date: Tue, 21 Nov 2023 22:30:33 +0100 Subject: [PATCH 4/9] use the customer map for the suppliers all over the connectors --- frepple/controllers/outbound.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/frepple/controllers/outbound.py b/frepple/controllers/outbound.py index c13fdf43..f0bec99b 100644 --- a/frepple/controllers/outbound.py +++ b/frepple/controllers/outbound.py @@ -719,16 +719,12 @@ def export_suppliers(self): res.partner.id res.partner.name -> supplier.name """ first = True - for i in self.generator.getData( - "res.partner", - search=[("is_company", "=", True)], - fields=["name"], - ): + for i in self.map_customers: if first: yield "\n" yield "\n" first = False - yield "\n" % quoteattr("%d %s" % (i["id"], i["name"])) + yield "\n" % quoteattr(self.map_customers[i]) if not first: yield "\n" @@ -948,7 +944,7 @@ def export_items(self): ) suppliers = {} for sup in results: - name = "%d %s" % (sup["name"][0], sup["name"][1]) + name = self.map_customers.get(sup["name"][0]) if sup.get("is_subcontractor", False): if not hasattr(tmpl, "subcontractors"): tmpl["subcontractors"] = [] @@ -1885,7 +1881,7 @@ def export_purchaseorders(self): qty, quoteattr(item["name"]), quoteattr(location), - quoteattr("%d %s" % (j["partner_id"][0], j["partner_id"][1])), + quoteattr(self.map_customers.get(j["partner_id"][0])), ) yield "\n" @@ -1963,7 +1959,7 @@ def export_purchaseorders(self): qty, quoteattr(item["name"]), quoteattr(location), - quoteattr("%d %s" % (j["partner_id"][0], j["partner_id"][1])), + quoteattr(self.map_customers.get(j["partner_id"][0])), ) yield "\n" From b492039857942ac576e2553db117a409a95b32b1 Mon Sep 17 00:00:00 2001 From: Hicham Lahlou Date: Wed, 22 Nov 2023 15:12:52 +0100 Subject: [PATCH 5/9] replace individual customers with "Individuals" name to prevent from pulling too many records in a b2c business --- frepple/controllers/outbound.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/frepple/controllers/outbound.py b/frepple/controllers/outbound.py index f0bec99b..c6db8288 100644 --- a/frepple/controllers/outbound.py +++ b/frepple/controllers/outbound.py @@ -689,24 +689,29 @@ def export_customers(self): """ self.map_customers = {} first = True + individual_inserted = False for i in self.generator.getData( "res.partner", search=[], - fields=["name", "parent_id"], + fields=["name", "parent_id", "is_company"], order="id", ): if first: yield "\n" yield "\n" first = False - if not i["parent_id"] or i["id"] == i["parent_id"][0]: + if i["is_company"]: name = "%s %s" % (i["name"], i["id"]) yield "\n" % quoteattr(name) - self.map_customers[i["id"]] = ( - name - if not i["parent_id"] or i["id"] == i["parent_id"][0] - else self.map_customers[i["parent_id"][0]] - ) + elif i["parent_id"] == False or i["id"] == i["parent_id"][0]: + name = "Individuals" + if not individual_inserted: + yield "\n" % quoteattr(name) + individual_inserted = True + else: + name = self.map_customers[i["parent_id"][0]] + + self.map_customers[i["id"]] = name if not first: yield "\n" From 596144ad9609f9803b588169735bb402afbca839 Mon Sep 17 00:00:00 2001 From: Johan De Taeye Date: Thu, 23 Nov 2023 14:51:33 +0100 Subject: [PATCH 6/9] keeping priority at 0 from MO-specific operations --- frepple/controllers/outbound.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frepple/controllers/outbound.py b/frepple/controllers/outbound.py index c6db8288..7380a3be 100644 --- a/frepple/controllers/outbound.py +++ b/frepple/controllers/outbound.py @@ -2149,7 +2149,7 @@ def export_manufacturingorders(self): if not wo_list: # There are no workorders on the manufacturing order - yield '' % ( + yield '' % ( quoteattr(operation), quoteattr(location), quoteattr(item["name"]), From cd9d9f2c7eaa13c8847d52a0fe10b7fa35df4fa4 Mon Sep 17 00:00:00 2001 From: Hicham Lahlou Date: Fri, 24 Nov 2023 10:49:06 +0100 Subject: [PATCH 7/9] do not include work center specific leaves/attendances into the generic calendar --- frepple/controllers/outbound.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frepple/controllers/outbound.py b/frepple/controllers/outbound.py index 7380a3be..22642963 100644 --- a/frepple/controllers/outbound.py +++ b/frepple/controllers/outbound.py @@ -493,7 +493,7 @@ def export_calendar(self): # Read the attendance for all calendars for i in self.generator.getData( "resource.calendar.attendance", - search=[("display_type", "=", False)], + search=[("display_type", "=", False), ("resource_id", "=", False)], fields=[ "dayofweek", "date_from", @@ -513,7 +513,7 @@ def export_calendar(self): # Read the leaves for all calendars for i in self.generator.getData( "resource.calendar.leaves", - search=[("time_type", "=", "leave")], + search=[("time_type", "=", "leave"), ("resource_id", "=", False)], fields=[ "date_from", "date_to", From 8417e70c1ff66b4fc685c009fbe400216899d7da Mon Sep 17 00:00:00 2001 From: Hicham Lahlou Date: Mon, 11 Dec 2023 15:12:01 +0100 Subject: [PATCH 8/9] specific calendars for work centers having leaves --- frepple/controllers/outbound.py | 118 +++++++++++++++++++++++++++++--- 1 file changed, 107 insertions(+), 11 deletions(-) diff --git a/frepple/controllers/outbound.py b/frepple/controllers/outbound.py index 22642963..768dfd07 100644 --- a/frepple/controllers/outbound.py +++ b/frepple/controllers/outbound.py @@ -490,10 +490,47 @@ def export_calendar(self): cal_tz[i["name"]] = i["tz"] cal_ids.add(i["id"]) + # Read the resource calendar association + calendar_resource = {} + for i in self.generator.getData( + "mrp.workcenter", + search=[("resource_calendar_id", "!=", False)], + fields=[ + "id", + "resource_calendar_id", + ], + ): + if i["resource_calendar_id"][0] not in calendar_resource: + calendar_resource[i["resource_calendar_id"][0]] = set() + calendar_resource[i["resource_calendar_id"][0]].add(i["id"]) + + # Read from the attendance/leaves which resource has specific entries + self.resources_with_specific_calendars = {} + for i in self.generator.getData( + "resource.calendar.attendance", + search=[("resource_id", "!=", False)], + fields=[ + "resource_id", + ], + ): + self.resources_with_specific_calendars[i["resource_id"][0]] = i[ + "resource_id" + ][1] + for i in self.generator.getData( + "resource.calendar.leaves", + search=[("resource_id", "!=", False), ("time_type", "=", "leave")], + fields=[ + "resource_id", + ], + ): + self.resources_with_specific_calendars[i["resource_id"][0]] = i[ + "resource_id" + ][1] + # Read the attendance for all calendars for i in self.generator.getData( "resource.calendar.attendance", - search=[("display_type", "=", False), ("resource_id", "=", False)], + search=[("display_type", "=", False)], fields=[ "dayofweek", "date_from", @@ -502,29 +539,81 @@ def export_calendar(self): "hour_to", "calendar_id", "week_type", + "resource_id", ], ): if i["calendar_id"] and i["calendar_id"][0] in cal_ids: - if i["calendar_id"][1] not in calendars: - calendars[i["calendar_id"][1]] = [] - i["attendance"] = True - calendars[i["calendar_id"][1]].append(i) + if not i["resource_id"]: + if i["calendar_id"][1] not in calendars: + calendars[i["calendar_id"][1]] = [] + i["attendance"] = True + calendars[i["calendar_id"][1]].append(i) + + if calendar_resource.get(i["calendar_id"][0]): + for res in calendar_resource.get(i["calendar_id"][0]): + if i["resource_id"] and res != i["resource_id"][0]: + continue + if res in self.resources_with_specific_calendars: + if ( + "calendar for %s" + % (self.resources_with_specific_calendars[res],) + not in calendars + ): + calendars[ + "calendar for %s" + % (self.resources_with_specific_calendars[res],) + ] = [] + cal_tz[ + "calendar for %s" + % (self.resources_with_specific_calendars[res],) + ] = cal_tz[i["calendar_id"][1]] + i["attendance"] = True + calendars[ + "calendar for %s" + % (self.resources_with_specific_calendars[res],) + ].append(i) # Read the leaves for all calendars for i in self.generator.getData( "resource.calendar.leaves", - search=[("time_type", "=", "leave"), ("resource_id", "=", False)], + search=[("time_type", "=", "leave")], fields=[ "date_from", "date_to", "calendar_id", + "resource_id", ], ): if i["calendar_id"] and i["calendar_id"][0] in cal_ids: - if i["calendar_id"][1] not in calendars: - calendars[i["calendar_id"][1]] = [] - i["attendance"] = False - calendars[i["calendar_id"][1]].append(i) + if not i["resource_id"]: + if i["calendar_id"][1] not in calendars: + calendars[i["calendar_id"][1]] = [] + i["attendance"] = False + calendars[i["calendar_id"][1]].append(i) + + if calendar_resource.get(i["calendar_id"][0]): + for res in calendar_resource.get(i["calendar_id"][0]): + if i["resource_id"] and res != i["resource_id"][0]: + continue + if res in self.resources_with_specific_calendars: + if ( + "calendar for %s" + % (self.resources_with_specific_calendars[res],) + not in calendars + ): + calendars[ + "calendar for %s" + % (self.resources_with_specific_calendars[res],) + ] = [] + cal_tz[ + "calendar for %s" + % (self.resources_with_specific_calendars[res],) + ] = cal_tz[i["calendar_id"][1]] + i["attendance"] = False + calendars[ + "calendar for %s" + % (self.resources_with_specific_calendars[res],) + ].append(i) # Iterate over the results: for i in calendars: @@ -805,7 +894,14 @@ def export_workcenters(self): first = False name = i["name"] owner = i["owner"] - available = i["resource_calendar_id"] + available = ( + i["resource_calendar_id"] + if not self.resources_with_specific_calendars.get(i["id"]) + else ( + 0, + "calendar for %s" % (name,), + ) + ) self.map_workcenters[i["id"]] = name yield '%s%s\n' % ( quoteattr(name), From 4b106a8180212c61ac25684c3bbeb94b7aaaa3a6 Mon Sep 17 00:00:00 2001 From: Johan De Taeye Date: Thu, 14 Dec 2023 15:16:02 +0100 Subject: [PATCH 9/9] black + mappng for mto items --- frepple/controllers/outbound.py | 55 ++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/frepple/controllers/outbound.py b/frepple/controllers/outbound.py index c9c93f9e..c119751c 100644 --- a/frepple/controllers/outbound.py +++ b/frepple/controllers/outbound.py @@ -645,7 +645,7 @@ def export_calendar(self): "1" if j["attendance"] else "0", (2 ** ((int(j["dayofweek"]) + 1) % 7)) if "dayofweek" in j - else (2 ** 7) - 1, + else (2**7) - 1, priority_attendance if j["attendance"] else priority_leave, # In odoo, monday = 0. In frePPLe, sunday = 0. ("PT%dM" % round(j["hour_from"] * 60)) @@ -678,7 +678,7 @@ def export_calendar(self): "1", (2 ** ((int(j["dayofweek"]) + 1) % 7)) if "dayofweek" in j - else (2 ** 7) - 1, + else (2**7) - 1, priority_attendance, # In odoo, monday = 0. In frePPLe, sunday = 0. ("PT%dM" % round(j["hour_from"] * 60)) @@ -944,6 +944,13 @@ def export_items(self): self.product_product = {} self.product_template_product = {} self.product_templates = {} + self.routes = { + i["id"]: i for i in self.generator.getData("stock.route", fields=["name"]) + } + self.route_mto = None + for k, v in self.routes.items(): + if v["name"] == "Replenish on Order (MTO)": + self.route_mto = k for i in self.generator.getData( "product.template", search=[("type", "not in", ("service", "consu"))], @@ -956,6 +963,7 @@ def export_items(self): "uom_id", "categ_id", "product_variant_ids", + "route_ids", ], ): self.product_templates[i["id"]] = i @@ -1010,8 +1018,7 @@ def export_items(self): } self.product_product[i["id"]] = prod_obj self.product_template_product[i["product_tmpl_id"][0]] = prod_obj - # For make-to-order items the next line needs to XML snippet ' type="item_mto"'. - yield '\n' % ( + yield '\n' % ( quoteattr(name), quoteattr(tmpl["uom_id"][1]) if tmpl["uom_id"] else "", i["volume"] or 0, @@ -1024,6 +1031,7 @@ def export_items(self): quoteattr(tmpl["categ_id"][1]) if tmpl["categ_id"] else '""', self.uom_categories[self.uom[tmpl["uom_id"][0]]["category"]], i["id"], + ' type="item_mto"' if self.route_mto in tmpl["route_ids"] else "" ) # Export suppliers for the item, if the item is allowed to be purchased if tmpl["purchase_ok"]: @@ -1546,24 +1554,29 @@ def export_boms(self): not in self.map_workcenters ): continue - secondary_workcenter_str += '%s' % ( - 1 - if not secondary_workcenter["duration"] - or step["time_cycle"] == 0 - else secondary_workcenter["duration"] - / step["time_cycle"], - quoteattr(secondary_workcenter["search_mode"]), - quoteattr( - self.map_workcenters[ - secondary_workcenter["workcenter_id"][0] - ] - ), - ( - "" - % quoteattr(secondary_workcenter["skill"][1]) + secondary_workcenter_str += ( + '%s' + % ( + 1 + if not secondary_workcenter["duration"] + or step["time_cycle"] == 0 + else secondary_workcenter["duration"] + / step["time_cycle"], + quoteattr(secondary_workcenter["search_mode"]), + quoteattr( + self.map_workcenters[ + secondary_workcenter["workcenter_id"][0] + ] + ), + ( + "" + % quoteattr( + secondary_workcenter["skill"][1] + ) + ) + if secondary_workcenter["skill"] + else "", ) - if secondary_workcenter["skill"] - else "", ) yield "" '\n' "\n" '%s%s\n' % (