From 62d6f3c60ffbcdeef6e161df983869f55df300d6 Mon Sep 17 00:00:00 2001 From: Hicham Lahlou Date: Fri, 9 Feb 2024 09:22:42 +0100 Subject: [PATCH 1/6] use internal reference as item name in frepple when some odoo product name are too long. --- frepple/controllers/outbound.py | 40 +++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/frepple/controllers/outbound.py b/frepple/controllers/outbound.py index f8e129c0..17f0d009 100644 --- a/frepple/controllers/outbound.py +++ b/frepple/controllers/outbound.py @@ -979,6 +979,30 @@ def export_items(self): ): self.product_templates[i["id"]] = i + # Check if we should use short names + # "[internal reference] name" will become "internal reference" + # and the frepple description will contain the odoo name + # To use short names, we have 2 conditions to check + # 1) The list of item names must contain a string which is more than 200 chars + # 2) The internal reference needs to be unique + product_codes = [] + use_short_names = False + max_name_length = 0 + for i in self.generator.getData( + "product.product", + search=[("product_tmpl_id.id", "in", list(self.product_templates))], + fields=[ + "name", + "code", + ], + ): + if len(i["name"]) > max_name_length: + max_name_length = len(i["name"]) + product_codes.append(i["code"] or i["name"]) + + if max_name_length > 200 and len(product_codes) == len(set(product_codes)): + use_short_names = True + # Read the products supplierinfo_fields = [ "name", @@ -1013,13 +1037,20 @@ def export_items(self): continue tmpl = self.product_templates[i["product_tmpl_id"][0]] if i["code"]: - name = ("[%s] %s" % (i["code"], i["name"]))[:300] + name = ( + (("[%s] %s" % (i["code"], i["name"]))[:300]) + if not use_short_names + else i["code"][:300] + ) + description = i["name"][:500] if use_short_names else None # product is a variant and has no internal reference # we use the product id as code elif i["product_template_attribute_value_ids"]: name = ("[%s] %s" % (i["id"], i["name"]))[:300] + description = i["name"][:500] if use_short_names else None else: name = i["name"][:300] + description = i["name"][:500] if use_short_names else None prod_obj = { "name": name, "template": i["product_tmpl_id"][0], @@ -1030,8 +1061,13 @@ 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), + ( + ("description=%s" % (quoteattr(description),)) + if use_short_names + else "" + ), quoteattr(tmpl["uom_id"][1]) if tmpl["uom_id"] else "", i["volume"] or 0, i["weight"] or 0, From 1b7f3260196273ae6391d78b1ceac48f81f804ae Mon Sep 17 00:00:00 2001 From: Hicham Lahlou Date: Fri, 9 Feb 2024 11:49:42 +0100 Subject: [PATCH 2/6] use short names => makes it default = True and use sql rather than an inefficient loop --- frepple/controllers/outbound.py | 45 ++++++++++++++++----------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/frepple/controllers/outbound.py b/frepple/controllers/outbound.py index 17f0d009..e5eed10c 100644 --- a/frepple/controllers/outbound.py +++ b/frepple/controllers/outbound.py @@ -979,29 +979,28 @@ def export_items(self): ): self.product_templates[i["id"]] = i - # Check if we should use short names - # "[internal reference] name" will become "internal reference" - # and the frepple description will contain the odoo name - # To use short names, we have 2 conditions to check - # 1) The list of item names must contain a string which is more than 200 chars - # 2) The internal reference needs to be unique - product_codes = [] - use_short_names = False - max_name_length = 0 - for i in self.generator.getData( - "product.product", - search=[("product_tmpl_id.id", "in", list(self.product_templates))], - fields=[ - "name", - "code", - ], - ): - if len(i["name"]) > max_name_length: - max_name_length = len(i["name"]) - product_codes.append(i["code"] or i["name"]) - - if max_name_length > 200 and len(product_codes) == len(set(product_codes)): - use_short_names = True + # Check if we can use short names + # To use short names, the internal reference (or the name when no internal reference is defined) + # needs to be unique + use_short_names = True + + self.generator.env.cr.execute( + """ + select count(*) from + ( + select coalesce(product_product.default_code, product_template.name), count(*) + from product_product + inner join product_template on product_product.product_tmpl_id = product_template.id + where product_template.type not in ('service', 'consu') + group by coalesce(product_product.default_code, product_template.name) + having count(*) > 1 + ) t + """ + ) + for i in self.generator.env.cr.fetchall(): + if i[0] > 0: + use_short_names = False + break # Read the products supplierinfo_fields = [ From 589f4feb20aa0e94e1ba73090591838ad325c9d7 Mon Sep 17 00:00:00 2001 From: Hicham Lahlou Date: Mon, 12 Feb 2024 08:02:03 +0100 Subject: [PATCH 3/6] fix merge issues --- frepple/controllers/outbound.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/frepple/controllers/outbound.py b/frepple/controllers/outbound.py index 72c63f4b..5fe77084 100644 --- a/frepple/controllers/outbound.py +++ b/frepple/controllers/outbound.py @@ -1022,11 +1022,11 @@ def export_items(self): """ select count(*) from ( - select coalesce(product_product.default_code, product_template.name), count(*) + select coalesce(product_product.default_code, product_template.name->>'en_US'), count(*) from product_product inner join product_template on product_product.product_tmpl_id = product_template.id where product_template.type not in ('service', 'consu') - group by coalesce(product_product.default_code, product_template.name) + group by coalesce(product_product.default_code, product_template.name->>'en_US') having count(*) > 1 ) t """ @@ -1095,8 +1095,7 @@ def export_items(self): 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), ( ("description=%s" % (quoteattr(description),)) From dc3d18957f1a272521393829a100d4ae0c490fc9 Mon Sep 17 00:00:00 2001 From: Hicham Lahlou Date: Mon, 12 Feb 2024 08:31:40 +0100 Subject: [PATCH 4/6] use language parameter to retrieve the product names when checking for internal reference uniqueness --- frepple/controllers/frepplexml.py | 1 + frepple/controllers/outbound.py | 13 ++++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/frepple/controllers/frepplexml.py b/frepple/controllers/frepplexml.py index bbdb3bc5..31077cdd 100644 --- a/frepple/controllers/frepplexml.py +++ b/frepple/controllers/frepplexml.py @@ -158,6 +158,7 @@ def xml(self, **kwargs): == "true", version=version, delta=float(kwargs.get("delta", 999)), + language=language, ) # last empty double quote is to let python understand frepple is a folder. xml_folder = os.path.join(str(Path.home()), "logs", "frepple", "") diff --git a/frepple/controllers/outbound.py b/frepple/controllers/outbound.py index 5fe77084..0e1a67a8 100644 --- a/frepple/controllers/outbound.py +++ b/frepple/controllers/outbound.py @@ -161,6 +161,7 @@ def __init__( singlecompany=False, version="0.0.0.unknown", delta=999, + language="en_US", ): self.database = database self.company = company @@ -185,6 +186,7 @@ def __init__( self.timeformat = "%Y-%m-%dT%H:%M:%S" self.singlecompany = singlecompany self.delta = delta + self.language = language # The mode argument defines different types of runs: # - Mode 1: @@ -1022,14 +1024,19 @@ def export_items(self): """ select count(*) from ( - select coalesce(product_product.default_code, product_template.name->>'en_US'), count(*) + select coalesce(product_product.default_code, + product_template.name->>%s, + product_template.name->>'en_US'), count(*) from product_product inner join product_template on product_product.product_tmpl_id = product_template.id where product_template.type not in ('service', 'consu') - group by coalesce(product_product.default_code, product_template.name->>'en_US') + group by coalesce(product_product.default_code, + product_template.name->>%s, + product_template.name->>'en_US') having count(*) > 1 ) t - """ + """, + (self.language, self.language), ) for i in self.generator.env.cr.fetchall(): if i[0] > 0: From 5c8e01111d92784d8346725262535db42f064e51 Mon Sep 17 00:00:00 2001 From: Hicham Lahlou Date: Mon, 12 Feb 2024 15:24:27 +0100 Subject: [PATCH 5/6] fix uom id that should be the product uom, not the category reference uom --- frepple/controllers/outbound.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/frepple/controllers/outbound.py b/frepple/controllers/outbound.py index e5eed10c..c7cdd2e1 100644 --- a/frepple/controllers/outbound.py +++ b/frepple/controllers/outbound.py @@ -334,7 +334,6 @@ def load_uom(self): unit of measure of the uom dimension. """ self.uom = {} - self.uom_categories = {} for i in self.generator.getData( "uom.uom", # We also need to load INactive UOMs, because there still might be records @@ -342,8 +341,6 @@ def load_uom(self): search=["|", ("active", "=", 1), ("active", "=", 0)], fields=["factor", "uom_type", "category_id", "name"], ): - if i["uom_type"] == "reference": - self.uom_categories[i["category_id"][0]] = i["id"] self.uom[i["id"]] = { "factor": i["factor"], "category": i["category_id"][0], @@ -1076,7 +1073,7 @@ def export_items(self): # max(0, tmpl["standard_price"]) or 0) # Option 2: Map the "cost" to frepple / self.convert_qty_uom(1.0, tmpl["uom_id"], i["product_tmpl_id"][0]), quoteattr(tmpl["categ_id"][1]) if tmpl["categ_id"] else '""', - self.uom_categories[self.uom[tmpl["uom_id"][0]]["category"]], + tmpl["uom_id"][0], i["id"], ) # Export suppliers for the item, if the item is allowed to be purchased From 2911dfaff021aea0e6bdeeddedef0b02b9eedfe1 Mon Sep 17 00:00:00 2001 From: Hicham Lahlou Date: Mon, 12 Feb 2024 16:17:40 +0100 Subject: [PATCH 6/6] it's black --- frepple/controllers/inbound.py | 22 +- frepple/controllers/outbound.py | 343 +++++++++++++++++++------------- 2 files changed, 213 insertions(+), 152 deletions(-) diff --git a/frepple/controllers/inbound.py b/frepple/controllers/inbound.py index fcc9e164..0015d9b1 100644 --- a/frepple/controllers/inbound.py +++ b/frepple/controllers/inbound.py @@ -345,17 +345,17 @@ def run(self): if not hasattr(self, "stock_picking_dict"): self.stock_picking_dict = {} if not self.stock_picking_dict.get((origin, destination)): - self.stock_picking_dict[ - (origin, destination) - ] = stck_picking.create( - { - "picking_type_id": picking_type_id, - "scheduled_date": date_shipping, - "location_id": location_id["id"], - "location_dest_id": location_dest_id["id"], - "move_type": "direct", - "origin": "frePPLe", - } + self.stock_picking_dict[(origin, destination)] = ( + stck_picking.create( + { + "picking_type_id": picking_type_id, + "scheduled_date": date_shipping, + "location_id": location_id["id"], + "location_dest_id": location_dest_id["id"], + "move_type": "direct", + "origin": "frePPLe", + } + ) ) sp = self.stock_picking_dict.get((origin, destination)) if not hasattr(self, "sm_dict"): diff --git a/frepple/controllers/outbound.py b/frepple/controllers/outbound.py index c7cdd2e1..367730a8 100644 --- a/frepple/controllers/outbound.py +++ b/frepple/controllers/outbound.py @@ -370,19 +370,19 @@ def load_operation_types(self): "name": i["name"], "code": i["code"], "sequence_code": i["sequence_code"], - "default_location_src_id": self.map_locations.get( - i["default_location_src_id"][0], None - ) - if i["default_location_src_id"] - else None, - "default_location_dest_id": self.map_locations.get( - i["default_location_dest_id"][0], None - ) - if i["default_location_dest_id"] - else None, - "warehouse_id": self.warehouses[i["warehouse_id"][0]] - if i["warehouse_id"] - else None, + "default_location_src_id": ( + self.map_locations.get(i["default_location_src_id"][0], None) + if i["default_location_src_id"] + else None + ), + "default_location_dest_id": ( + self.map_locations.get(i["default_location_dest_id"][0], None) + if i["default_location_dest_id"] + else None + ), + "warehouse_id": ( + self.warehouses[i["warehouse_id"][0]] if i["warehouse_id"] else None + ), } def convert_qty_uom(self, qty, uom_id, product_template_id=None): @@ -637,32 +637,42 @@ def export_calendar(self): if j.get("week_type", False) == False: # ONE-WEEK CALENDAR yield '\n' % ( - self.formatDateTime(j["date_from"], cal_tz[i]) - if not j["attendance"] - else ( - j["date_from"].strftime("%Y-%m-%dT00:00:00") - if j["date_from"] - else "2020-01-01T00:00:00" + ( + self.formatDateTime(j["date_from"], cal_tz[i]) + if not j["attendance"] + else ( + j["date_from"].strftime("%Y-%m-%dT00:00:00") + if j["date_from"] + else "2020-01-01T00:00:00" + ) ), - self.formatDateTime(j["date_to"], cal_tz[i]) - if not j["attendance"] - else ( - j["date_to"].strftime("%Y-%m-%dT00:00:00") - if j["date_to"] - else "2030-12-31T00:00:00" + ( + self.formatDateTime(j["date_to"], cal_tz[i]) + if not j["attendance"] + else ( + j["date_to"].strftime("%Y-%m-%dT00:00:00") + if j["date_to"] + else "2030-12-31T00:00:00" + ) ), "1" if j["attendance"] else "0", - (2 ** ((int(j["dayofweek"]) + 1) % 7)) - if "dayofweek" in j - else (2**7) - 1, + ( + (2 ** ((int(j["dayofweek"]) + 1) % 7)) + if "dayofweek" in j + 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)) - if "hour_from" in j - else "PT0M", - ("PT%dM" % round(j["hour_to"] * 60)) - if "hour_to" in j - else "PT1440M", + ( + ("PT%dM" % round(j["hour_from"] * 60)) + if "hour_from" in j + else "PT0M" + ), + ( + ("PT%dM" % round(j["hour_to"] * 60)) + if "hour_to" in j + else "PT1440M" + ), ) if j["attendance"]: priority_attendance += 1 @@ -683,17 +693,23 @@ def export_calendar(self): cal_tz[i], ), "1", - (2 ** ((int(j["dayofweek"]) + 1) % 7)) - if "dayofweek" in j - else (2**7) - 1, + ( + (2 ** ((int(j["dayofweek"]) + 1) % 7)) + if "dayofweek" in j + else (2**7) - 1 + ), priority_attendance, # In odoo, monday = 0. In frePPLe, sunday = 0. - ("PT%dM" % round(j["hour_from"] * 60)) - if "hour_from" in j - else "PT0M", - ("PT%dM" % round(j["hour_to"] * 60)) - if "hour_to" in j - else "PT1440M", + ( + ("PT%dM" % round(j["hour_from"] * 60)) + if "hour_from" in j + else "PT0M" + ), + ( + ("PT%dM" % round(j["hour_to"] * 60)) + if "hour_to" in j + else "PT1440M" + ), ) priority_attendance += 1 dow = t.weekday() @@ -1155,13 +1171,18 @@ def export_items(self): v["batching_window"] or 0, v["min_qty"], max(0, v["price"]), - ' effective_end="%sT00:00:00"' - % v["date_end"].strftime("%Y-%m-%d") - if v["date_end"] - else "", - ' effective_start="%sT00:00:00"' % k[1].strftime("%Y-%m-%d") - if k[1] - else "", + ( + ' effective_end="%sT00:00:00"' + % v["date_end"].strftime("%Y-%m-%d") + if v["date_end"] + else "" + ), + ( + ' effective_start="%sT00:00:00"' + % k[1].strftime("%Y-%m-%d") + if k[1] + else "" + ), quoteattr(k[0]), ) yield "\n" @@ -1300,9 +1321,11 @@ def export_boms(self): if subcontractor: yield '\n' "\n" % ( quoteattr(operation), - ("description=%s " % quoteattr(i["code"])) - if i["code"] - else "", + ( + ("description=%s " % quoteattr(i["code"])) + if i["code"] + else "" + ), quoteattr(subcontractor["name"]), subcontractor.get("delay", 0), self.po_lead, @@ -1318,12 +1341,16 @@ def export_boms(self): yield '\n' "\n" % ( quoteattr(operation), - ("description=%s " % quoteattr(i["code"])) - if i["code"] - else "", - self.convert_float_time(duration_per) - if duration_per and duration_per > 0 - else "P0D", + ( + ("description=%s " % quoteattr(i["code"])) + if i["code"] + else "" + ), + ( + self.convert_float_time(duration_per) + if duration_per and duration_per > 0 + else "P0D" + ), self.manufacturing_lead, 100 + (i["sequence"] or 1), quoteattr(product_buf["name"]), @@ -1411,9 +1438,11 @@ def export_boms(self): if not product: continue yield '\n' % ( - "flow_fixed_end" - if j["subproduct_type"] == "fixed" - else "flow_end", + ( + "flow_fixed_end" + if j["subproduct_type"] == "fixed" + else "flow_end" + ), self.convert_qty_uom( j["product_qty"], j["product_uom"], @@ -1442,9 +1471,11 @@ def export_boms(self): quoteattr( self.map_workcenters[j["workcenter_id"][0]] ), - ("" % quoteattr(j["skill"][1])) - if j["skill"] - else "", + ( + ("" % quoteattr(j["skill"][1])) + if j["skill"] + else "" + ), ) # create a load for secondary workcenters # prepare the secondary workcenter xml string upfront @@ -1454,11 +1485,13 @@ def export_boms(self): sw_id ] yield '%s' % ( - 1 - if not secondary_workcenter["duration"] - or j["time_cycle"] == 0 - else secondary_workcenter["duration"] - / j["time_cycle"], + ( + 1 + if not secondary_workcenter["duration"] + or j["time_cycle"] == 0 + else secondary_workcenter["duration"] + / j["time_cycle"] + ), quoteattr(secondary_workcenter["search_mode"]), quoteattr( self.map_workcenters[ @@ -1466,13 +1499,15 @@ def export_boms(self): ] ), ( - "" - % quoteattr( - secondary_workcenter["skill"][1] + ( + "" + % quoteattr( + secondary_workcenter["skill"][1] + ) ) - ) - if secondary_workcenter["skill"] - else "", + if secondary_workcenter["skill"] + else "" + ), ) if exists: @@ -1484,9 +1519,11 @@ def export_boms(self): # yield '\n' % ( quoteattr(operation), - ("description=%s " % quoteattr(i["code"])) - if i["code"] - else "", + ( + ("description=%s " % quoteattr(i["code"])) + if i["code"] + else "" + ), self.manufacturing_lead, 100 + (i["sequence"] or 1), quoteattr(product_buf["name"]), @@ -1547,9 +1584,11 @@ def export_boms(self): fl[ ( j["product_id"][0], - j["operation_id"][0] - if j["operation_id"] - else None, + ( + j["operation_id"][0] + if j["operation_id"] + else None + ), ) ]["qty"] += qty else: @@ -1557,9 +1596,11 @@ def export_boms(self): fl[ ( j["product_id"][0], - j["operation_id"][0] - if j["operation_id"] - else None, + ( + j["operation_id"][0] + if j["operation_id"] + else None + ), ) ] = j @@ -1600,11 +1641,13 @@ def export_boms(self): secondary_workcenter_str += ( '%s' % ( - 1 - if not secondary_workcenter["duration"] - or step["time_cycle"] == 0 - else secondary_workcenter["duration"] - / step["time_cycle"], + ( + 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[ @@ -1612,34 +1655,42 @@ def export_boms(self): ] ), ( - "" - % quoteattr( - secondary_workcenter["skill"][1] + ( + "" + % quoteattr( + secondary_workcenter["skill"][1] + ) ) - ) - if secondary_workcenter["skill"] - else "", + if secondary_workcenter["skill"] + else "" + ), ) ) yield "" '\n' "\n" '%s%s\n' % ( quoteattr(name), - ("description=%s " % quoteattr(i["code"])) - if i["code"] - else "", + ( + ("description=%s " % quoteattr(i["code"])) + if i["code"] + else "" + ), counter * 10, - self.convert_float_time(step["time_cycle"] / 1440.0) - if step["time_cycle"] and step["time_cycle"] > 0 - else "P0D", + ( + self.convert_float_time(step["time_cycle"] / 1440.0) + if step["time_cycle"] and step["time_cycle"] > 0 + else "P0D" + ), quoteattr(location), 1, quoteattr(step["search_mode"]), quoteattr( self.map_workcenters[step["workcenter_id"][0]] ), - ("" % quoteattr(step["skill"][1])) - if step["skill"] - else "", + ( + ("" % quoteattr(step["skill"][1])) + if step["skill"] + else "" + ), secondary_workcenter_str, ) first_flow = True @@ -1835,9 +1886,11 @@ def getReservedQuantity(stock_move_id): ) % ( quoteattr(sol_name), quoteattr(batch), - qty - reserved_quantity - if qty - reserved_quantity > 0 - else qty, + ( + qty - reserved_quantity + if qty - reserved_quantity > 0 + else qty + ), due, priority, j["picking_policy"] == "one" and qty or 0.0, @@ -2084,9 +2137,9 @@ def export_purchaseorders(self): fields=["production_id"], ): if k["production_id"]: - self.subcontracting_mo_po_mapping[ - k["production_id"][0] - ] = po_line_reference + self.subcontracting_mo_po_mapping[k["production_id"][0]] = ( + po_line_reference + ) continue item = self.product_product.get(i["product_id"][0], None) if not item: @@ -2141,24 +2194,26 @@ def export_manufacturingorders(self): search=[("state", "in", ["progress", "confirmed", "to_close"])], # Option 2: Also import draft manufacturing order from odoo (to avoid that frepple reproposes it another time) # search=[("state", "in", ["draft", "progress", "confirmed", "to_close"])], - fields=[ - "bom_id", - "date_start", - "date_planned_start", - "date_planned_finished", - "name", - "state", - "qty_producing", - "product_qty", - "product_uom_id", - "location_dest_id", - "product_id", - "move_raw_ids", - "picking_type_id", - ] - + ["workorder_ids"] - if self.manage_work_orders - else [], + fields=( + [ + "bom_id", + "date_start", + "date_planned_start", + "date_planned_finished", + "name", + "state", + "qty_producing", + "product_qty", + "product_uom_id", + "location_dest_id", + "product_id", + "move_raw_ids", + "picking_type_id", + ] + + ["workorder_ids"] + if self.manage_work_orders + else [] + ), ): # Filter out irrelevant manufacturing orders location = self.map_locations.get(i["location_dest_id"][0], None) @@ -2329,10 +2384,9 @@ def export_manufacturingorders(self): self.product_product[mv["product_id"][0]]["template"], ) if qty_flow > 0: - operation_materials[ - consumed_item["name"] - ] = operation_materials.get(consumed_item["name"], 0) + ( - -qty_flow / qty + operation_materials[consumed_item["name"]] = ( + operation_materials.get(consumed_item["name"], 0) + + (-qty_flow / qty) ) for key in operation_materials: yield '\n' % ( @@ -2450,9 +2504,12 @@ def export_manufacturingorders(self): != wo["workcenter_id"][0] ): yield '' % ( - secondary["duration"] / wo["duration_expected"] - if secondary["duration"] and wo["duration_expected"] - else 1, + ( + secondary["duration"] / wo["duration_expected"] + if secondary["duration"] + and wo["duration_expected"] + else 1 + ), quoteattr( self.map_workcenters[ secondary["workcenter_id"][0] @@ -2490,11 +2547,15 @@ def export_manufacturingorders(self): dt = now else: dt = max( - wo["date_start"] - if wo["date_start"] - else wo["date_planned_start"] - if wo["date_planned_start"] - else i["date_planned_start"], + ( + wo["date_start"] + if wo["date_start"] + else ( + wo["date_planned_start"] + if wo["date_planned_start"] + else i["date_planned_start"] + ) + ), now, ) wo_date = ' start="%s"' % self.formatDateTime(dt)