Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1import datetime 

2import functools 

3import json 

4import logging 

5from collections import deque, namedtuple 

6 

7from sdc_etl_libs.sdc_file_helpers.TechnicalStandards.EDI.SDCEDIExceptions import \ 

8 EDITransactionNotFound 

9 

10 

11class SDCEDIObject: 

12 

13 Segment = namedtuple('Segment', ['SEGMENT_ID', 'POSITION', 'DETAIL']) 

14 FIELD_SEPARATOR = '~' 

15 SEGMENT_SEPARATOR = "|" 

16 

17 def __init__(self, file_obj_, field_separator_=FIELD_SEPARATOR, segment_separator_=SEGMENT_SEPARATOR): 

18 """ 

19 Creates an Python object representing a EDI file 

20 :param file_obj_: Raw file object 

21 :return: SDCEDIFile 

22 """ 

23 self.field_separator = field_separator_ 

24 self.segment_separator = segment_separator_ 

25 self.segments = self._extract_segments(file_obj_) 

26 

27 @staticmethod 

28 def _get_segment_name(segment_id: str, position: int) -> str: 

29 """ 

30 Make key joining segment_id and position 

31 :param segment_id: str 

32 :param position: int 

33 :return: str 

34 """ 

35 number = str(position + 1).zfill(2) 

36 return '{0}{1}'.format(segment_id, number) 

37 

38 @staticmethod 

39 def _extract_separator(edi_lines) -> str: 

40 """ 

41 Extract character separator 

42 :param edi_lines: list[str] 

43 :return: str 

44 """ 

45 return edi_lines[0][3:4] 

46 

47 def _extract_segment(self, enumeration_token): 

48 """ 

49 Build an instance of Segment class by parsing an edi text line 

50 :param enumeration_token:str 

51 :return: Segment 

52 """ 

53 # get position and text token 

54 position, tokens = enumeration_token 

55 # build null object if text is empty 

56 if not tokens: 

57 return self.Segment(SEGMENT_ID='Nil', POSITION=-1, DETAIL={}) 

58 # converts list to cons list 

59 queue_tokens = deque(tokens) 

60 # extract head and tail 

61 # head is edi segment, tail are segment attributes 

62 head, tail = queue_tokens.popleft(), queue_tokens 

63 # convert text fields to dict 

64 tail_detail = [{self._get_segment_name(head, i): field} for i, field in enumerate(queue_tokens)] 

65 # reduce by dict to consolidate dicts 

66 detail = functools.reduce(lambda dict_a, dict_b: dict(dict_a, **dict_b), tail_detail, {}) 

67 # return Segment object 

68 return self.Segment(SEGMENT_ID=head, POSITION=position, DETAIL=detail) 

69 

70 def _find_segment(self, segment_id: str): 

71 """ 

72 Searches segments by id 

73 :param segment_id:str 

74 :return: list[Segment] 

75 """ 

76 return list(filter(lambda segment: segment.SEGMENT_ID == segment_id, self.segments)) 

77 

78 def _convert_segment_to_json(self, segment_id): 

79 """ 

80 Converts a segment into json 

81 :param segment_id:str 

82 :return: JSON object 

83 """ 

84 return json.dumps([segment.DETAIL for segment in self._find_segment(segment_id)]) 

85 

86 def _extract_segments(self, file_obj): 

87 """ 

88 Build a list of objects of type Segment 

89 :param file_obj:StringIO 

90 :return: list[Segment] 

91 """ 

92 # move cursor to the first position in StringIO stream 

93 file_obj.seek(0) 

94 # extract edi_lines 

95 # new line is defined as \\n 

96 edi_lines = [segment for segment in file_obj.read().decode().splitlines() 

97 ] if self.segment_separator == '\\n' else [ 

98 segment for segment in file_obj.read().decode().split(self.segment_separator) 

99 ] 

100 # extract separator 

101 #separator = self._extract_separator(edi_lines) - alternative algorithm 

102 separator = self.field_separator 

103 # extract segments as tokens 

104 edi_lines_tokens = [segment.split(separator) for segment in edi_lines] 

105 # identify position of each token 

106 edi_lines_tokens_with_pos = [(pos, value) for pos, value in enumerate(edi_lines_tokens)] 

107 # extract segments from tokens 

108 return list(map(self._extract_segment, edi_lines_tokens_with_pos)) 

109 

110 def get_transactions_groups(self): 

111 """ 

112 Group by transactions 

113 :return: list[Segment] 

114 """ 

115 # detail section 

116 # find the beginning of each transaction 

117 list_edi_st = self._find_segment('ST') 

118 # find the end for of each transaction 

119 list_edi_se = self._find_segment('SE') 

120 # find start position of each transaction groups 

121 edi_invoices_positions = [segment.POSITION for segment in list_edi_st] 

122 # find last transaction 

123 last_se = list_edi_se[len(list_edi_se) - 1] 

124 # add last transaction position to the list of positions 

125 edi_invoices_positions.append(last_se.POSITION) 

126 # create a list of intervals 

127 transaction_intervals = list(zip(edi_invoices_positions, edi_invoices_positions[1:])) 

128 # extract transaction groups using intervals 

129 transaction_groups = [self.segments[start:end] for start, end in transaction_intervals] 

130 return transaction_groups 

131 

132 def get_headers(self): 

133 """ 

134 Get Segment object with headers information 

135 :return: list[Segment] 

136 """ 

137 isa_query = self._find_segment('ISA') 

138 gs_query = self._find_segment('GS') 

139 ge_query = self._find_segment('GE') 

140 iea_query = self._find_segment('IEA') 

141 

142 if gs_query: 

143 gs = gs_query[0] 

144 else: 

145 gs = None 

146 return [isa_query[0], gs, ge_query[0], iea_query[0]] 

147 

148 def get_data_date(self): 

149 """ 

150 Get origin date 

151 :return: datetime 

152 """ 

153 try: 

154 # parse segment B3 

155 list_edi_b3 = self._find_segment('B3') 

156 # segment B3 position 6 contains the date 

157 data_date = list_edi_b3[0].DETAIL['B306'] 

158 return datetime.datetime.strptime(data_date, '%Y%m%d').strftime("%Y-%m-%d %H:%M:%S.%f") 

159 except Exception as e: 

160 raise EDITransactionNotFound("Transactions not found") 

161 

162 @staticmethod 

163 def convert_segment_to_edi(segment, field_separator=FIELD_SEPARATOR, segment_separator=SEGMENT_SEPARATOR): 

164 """ 

165 Transform a Segment Object to EDI text 

166 :param segment:Segment 

167 :param field_separator:str 

168 :param segment_separator:str 

169 :return: list[Segment] 

170 """ 

171 segment_fields = [field_separator + segment.DETAIL[field] for field in segment.DETAIL] 

172 return segment.SEGMENT_ID + (''.join(segment_fields)) + segment_separator 

173 

174 def get_edi_type(self): 

175 """ 

176 Extract type of EDI file 

177 :return: str 

178 """ 

179 # edi type is the first field in a ST transaction 

180 return self._find_segment('ST')[0].DETAIL['ST01'] 

181 

182 def get_ack_997_msg(self): 

183 """ 

184 Extract segments that conform the response EDI 997 

185 :return: list[Segment] 

186 """ 

187 # 997 EDI segments 

188 

189 # ISA 

190 isa = self._find_segment('ISA') 

191 icn = isa[0].DETAIL['ISA13'] 

192 

193 # extract sender ID 

194 interchange_id_qualifier_sender = isa[0].DETAIL['ISA05'] 

195 interchange_sender_id = isa[0].DETAIL['ISA06'] 

196 # extract receiver ID 

197 interchange_id_qualifier_receiver = isa[0].DETAIL['ISA07'] 

198 interchange_receiver_id = isa[0].DETAIL['ISA08'] 

199 

200 # inverse 210 destinations 

201 isa[0].DETAIL['ISA05'] = interchange_id_qualifier_receiver 

202 isa[0].DETAIL['ISA06'] = interchange_receiver_id 

203 isa[0].DETAIL['ISA07'] = interchange_id_qualifier_sender 

204 isa[0].DETAIL['ISA08'] = interchange_sender_id 

205 

206 # GS 

207 gs = self._find_segment('GS') 

208 if len(gs) > 0: 

209 gs[0].DETAIL['GS01'] = 'FA' 

210 gs_number = gs[0].DETAIL['GS06'] 

211 gs = gs[0] 

212 else: 

213 gs = None 

214 gs_number = '0' 

215 

216 # ST 

217 st = self.Segment(SEGMENT_ID='ST', POSITION=2, DETAIL={'ST01': '997', 'ST02': icn}) 

218 

219 # AK1 

220 ak1 = self.Segment(SEGMENT_ID='AK1', POSITION=3, DETAIL={'AK101': 'IM', 'AK102': gs_number}) 

221 

222 # AK9 

223 number_of_transactions = str(len(self._find_segment('ST'))) 

224 ak9 = self.Segment( 

225 SEGMENT_ID='AK9', 

226 POSITION=3, 

227 DETAIL={ 

228 'AK101': 'A', 

229 'AK102': number_of_transactions, 

230 'AK103': number_of_transactions, 

231 'AK104': number_of_transactions 

232 }) 

233 

234 # SE 

235 se = self.Segment(SEGMENT_ID='SE', POSITION=4, DETAIL={'SE01': '4', 'SE02': icn}) 

236 

237 # GE 

238 ge = self.Segment(SEGMENT_ID='GE', POSITION=4, DETAIL={'GE01': '1', 'GE02': icn}) 

239 

240 # IEA 

241 iea = self._find_segment('IEA') 

242 

243 list_ack_segments = [isa[0]] 

244 if gs: 

245 list_ack_segments.append(gs) 

246 

247 list_ack_segments.append(st) 

248 list_ack_segments.append(ak1) 

249 list_ack_segments.append(ak9) 

250 list_ack_segments.append(se) 

251 list_ack_segments.append(ge) 

252 list_ack_segments.append(iea[0]) 

253 return list_ack_segments 

254 

255 @staticmethod 

256 def object_to_edi(list_segments) -> str: 

257 """ 

258 Transform an EDI Object to EDI text 

259 :param list_segments:list[Segment] 

260 :return: str 

261 """ 

262 segments_format_edi = [SDCEDIObject.convert_segment_to_edi(segment) for segment in list_segments] 

263 return ''.join(segments_format_edi)