def get_rechnung(rechnungsnr): """Findet eine Rechnung anhand ihrer Rechnungsnr""" conditions = ["FKSTAT <> 'X'", "FKRGNR = %d" % remove_prefix(rechnungsnr, 'RG')] rows = query(tables=['AFK00'], condition=' AND '.join(conditions), limit=1, ua='husoftm2.rechnungen') if not rows: return rechnung = rows[0] # Rechnungspositionen conditions = ["FURGNR <> 'X'"] rows = query(tables=['AFU00'], condition='FURGNR = %d' % remove_prefix(rechnungsnr, 'RG'), ua='husoftm2.rechnungen') rechnung['positionen'] = rows # Evtl. in Funktion auslagern wie in husoftm2.kunden: rechnung['rechnungsnr'] = add_prefix(rechnung['rechnungsnr'], 'RG') rechnung['auftragsnr'] = add_prefix(rechnung['auftragsnr'], 'SO') rechnung['kundennr_rechnungsempfaenger'] = add_prefix(rechnung['kundennr_rechnungsempfaenger'], 'SC') rechnung['kundennr_warenempfaenger'] = add_prefix(rechnung['kundennr_warenempfaenger'], 'SC') if rechnung['verbandsnr']: rechnung['verbandsnr'] = add_prefix(rechnung['verbandsnr'], 'SC') return rechnung
def _get_umsatz(kundennr, jahr, kundenfeld): """Ermittle den Umsatz im gegebenen Jahr""" if isinstance(jahr, datetime.date): jahr = jahr.year elif isinstance(jahr, basestring): jahr = int(jahr) # Das ist deutlich schneller als ein IN-Operator in der eigentlichen Abfrage kunde = husoftm2.kunden.get_kunde(kundennr) if kunde['land'] == 'DE': ktonr = 85100 elif huTools.world.in_european_union(kunde['land']): ktonr = 82100 else: ktonr = 81100 conditions = ['BUJJBU=%d' % (jahr - 2000), # Keinen Kommentar zu SoftM! 'BUGKTO=%s' % pad('BUGKTO', ktonr), 'BUPKTO=%s' % pad('BUPKTO', remove_prefix(kundennr, 'SC')), ] rows = query(tables=['BBU00'], fields=['BUSOHA', 'SUM(BUNEBT)'], grouping=['BUSOHA'], querymappings={'BUSOHA': 'art', 'SUM(BUNEBT)': 'umsatz'}, condition=' AND '.join(conditions), ua='husoftm2.umsatz.get_umsatz', cachingtime=86400) umsatz = decimal.Decimal() for row in rows: if row['art'] == 'H': umsatz -= decimal.Decimal(row['umsatz']) elif row['art'] == 'S': umsatz += decimal.Decimal(row['umsatz']) return umsatz
def mark_processed(lieferscheinnr): """Markiert einen Lieferschein, so dass er von get_new() nicht mehr zurücuk gegeben wird.""" conditions = ["LKLFSN=%s" % sql_quote(remove_prefix(lieferscheinnr, 'SL')), "LKSTAT<>'X'", "LKKZ02=0", ] return x_en('ALK00', condition=' AND '.join(conditions), ua='husoftm2.lieferscheine')
def kundenpreise(kundennr, gueltig_von=None, gueltig_bis=None): """Alle kundenspezifischen Preis für einen Kunden""" kundennr = remove_prefix(kundennr, 'SC') conditions = ["PNSANR=PRSANR", "PRANW='A'", "PRSTAT=' '", "PNSTAT=' '", "PRKDNR=%s" % pad('PRKDNR', kundennr) ] if gueltig_von: conditions.append("PRDTVO>=%s" % sql_quote(date2softm(gueltig_von))) if gueltig_bis: conditions.append("PRDTBI>=%s" % sql_quote(date2softm(gueltig_bis))) rows = query(tables=['XPN00', 'XPR00'], fields=['PRARTN', 'PNPRB', 'PRDTVO', 'PRDTBI', 'PRSANR'], condition=' AND '.join(conditions), ordering='PRDTVO' ) preise = {} for row in rows: preise[row['artnr']] = dict(preis=int(row['preis'] * 100), gueltig_von=row.get('gueltig_ab_date'), gueltig_bis=row.get('gueltig_bis_date'), satznr=str(row['satznr_xpr00'])) return preise
def get_kommibeleg(komminr, header_only=False): """Gibt einen Kommissionierbeleg zurück""" prefix = 'KA' if komminr.startswith('KB'): prefix = 'KB' komminr = remove_prefix(komminr, prefix) # In der Tabelle ALK00 stehen Kommissionierbelege und Lieferscheine. # Die Kommissionierbelege haben '0' als Lieferscheinnr. # Zusätzlich werden die (logisch) gelöschten Lieferscheine rausgefiltert. conditions = ["LKLFSN = 0", "LKKBNR = %s" % sql_quote(komminr), "LKSTAT<>'X'"] try: belege = get_ls_kb_data(conditions, header_only=header_only, is_lieferschein=False) except RuntimeError: return {} if belege: beleg = belege[0] # Falls es bereits einen Lieferschein gibt, die Lieferscheinnr in das dict schreiben. # Ansonsten die Eintrag 'lieferscheinnr' entfernen (wäre sonst SL0) rows = query(['ALK00'], condition="LKLFSN <> 0 AND LKKBNR = %s" % sql_quote(komminr)) if rows: beleg['lieferscheinnr'] = rows[0]['lieferscheinnr'] else: beleg.pop('lieferscheinnr', None) return beleg return {}
def auftragstextdaten(auftragsnr): """Auftrags- und Positionstexte und -daten für einen Auftrag zurückliefern.""" auftragsnr = "SO%s" % remove_prefix(auftragsnr, 'SO') postexte, kopftexte, posdaten, kopfdaten = txt_auslesen([auftragsnr]) return (postexte.get(auftragsnr, {}), kopftexte.get(auftragsnr, []), posdaten.get(auftragsnr, {}), kopfdaten.get(auftragsnr, {}))
def get_auftraege_by_auftragsnrs(auftragsnrs, header_only=False, canceled=False): """Aufträge mit bestimmten Auftragsnummern zurückgeben. Wenn `canceled == False` werden keine stornierten Aufträge und Positionen zurückgegeben.""" auftragsnrs = [sql_escape(remove_prefix(x, 'SO')) for x in auftragsnrs] auftraege = _auftraege(["AKAUFN IN (%s)" % ','.join(auftragsnrs)], header_only=header_only, canceled=canceled) return auftraege
def durchschnittlicher_abgabepreis(artnr, kundennr=None, startdatum=None): """Gibt eine Liste mit den durchschnittlichen Rechnungspreisen pro Monat zurück. Liefert eine Liste von 4-Tuples (datum, AVG(preis), menge, umsatz) Wenn eine Kundennummer mitgeliefert wird, werden nur Rechungen für diesen Kunden betrachtet. Wenn ein startdatum angegebenw wird, werden nur vorgänge nach diesem Datum betrachtet. [ ... (datetime.date(2009, 2, 1), 3295, 2, 6590), (datetime.date(2009, 10, 1), 1744, 2, 3488)] Die Funktion ist ausgesprochen langsam - bis zu 8 Sekunden. """ conditions = [ "FUARTN=%s" % (sql_quote(artnr)), # nur bestimmten Artikel beachten "FKRGNR=FURGNR", # JOIN "FKAUFA<>'U'", # Keine Umlagerung "FKSTAT<>'X'", # nicht gelöscht "FKDTFA>0", # Druckdatum nicht leer "FUKZRV=2", # ? "FURGNI<>0", # ? "FKFORM=' '", # ? "FURGNR<>0", # es gibt eine Rechnungsnummer "FUPNET>0", # keine Gutschriften ] if kundennr: kundennr = remove_prefix(kundennr, 'SC') conditions = ["(FKKDNR=%s OR FKKDRG=%s)" % (sql_quote('%8s' % kundennr), sql_quote('%8s' % kundennr))] + conditions if not startdatum: conditions = ["FKDTFA>'10501'"] + conditions # keine legacy Daten else: conditions = ["FKDTFA>%s" % sql_quote(date2softm(startdatum))[:5]] + conditions rows = query(['AFU00', 'AFK00'], fields=["FKDTFA", 'SUM(FUMNG)', 'SUM(FUPNET)', 'COUNT(FKRGNR)'], condition=' AND '.join(conditions), grouping='FKDTFA', cachingtime=60 * 60 * 24 * 3, querymappings={'SUM(FUMNG)': 'menge', 'SUM(FUPNET)': 'nettopreis', 'COUNT(FKRGNR)': 'rechnungen', 'FKDTFA': 'rechnung_date'}) mengen = {} umsatz = {} for row in rows: menge = int(float(row['menge'])) nettopreis = float(row['nettopreis']) * 100 if menge: datum = datetime.date(row['rechnung_date'].year, row['rechnung_date'].month, 1) if datum not in mengen: mengen[datum] = umsatz[datum] = 0 mengen[datum] += int(menge) umsatz[datum] += int(nettopreis) ret = [] for datum in sorted(mengen.keys()): ret.append((datum, int(umsatz[datum] / mengen[datum]), mengen[datum], umsatz[datum])) return ret
def offene_auftraege_kunde(kundennr, limit=None, header_only=False, canceled=False): """Alle nicht abgeschlossene Aufträge für eine Kundennummer ermitteln. Gibt eine Liste von dict()s zurück. Wenn `canceled == False` werden keine stornierten Aufträge und Positionen zurückgegeben.""" kundennr = remove_prefix(kundennr, 'SC') auftraege = _auftraege(["AKKDNR=%s" % pad('AKKDNR', kundennr), "AKKZVA=0"], limit=limit, header_only=header_only, canceled=canceled) return auftraege
def lieferschein_for_kommiauftrag(komminr, header_only=False): """Gibt den zu dem Kommiauftrag passenden Lieferschein zurück Falls der Lieferschein (noch) nicht existiert, wird None zurückgegeben """ komminr = remove_prefix(komminr, 'KA') lieferscheine = _lieferscheine(["LKKBNR = %s" % sql_quote(komminr)], limit=1, header_only=header_only) if lieferscheine: return lieferscheine[0]
def get_auftragsarten_by_auftragsnrs(auftragsnrs): """Ermittelt die Auftragsart (Freitext) der Aufträge der gegebenen Auftragsnummern. auftragsnrs: liste von Auftragsnummern return: Dictionary Auftragnr -> Auftragsart """ condition = "AKAUFN in (%s)" % ','.join(sql_quote(remove_prefix(nr, 'SO')) for nr in auftragsnrs) rows = query(['AAK00'], fields=['AKAUFN', 'AKAUFA'], condition=condition) return dict(("SO%s" % row['auftragsnr'], AUFTRAGSARTEN[row['art']]) for row in rows)
def get_guid(auftragsnr): """Gibt den GUID zu einer Auftragsnr zurück, sofern vorhanden.""" auftragsnr = remove_prefix(auftragsnr, 'SO') condition = "ATTX60 LIKE %s AND ATAUFN = %s AND ATAUPO = 0 AND ATTART = 8" % (sql_quote("#:guid:%%"), sql_quote(auftragsnr)) rows = query('AAT00', fields=['ATTX60'], condition=condition) if rows: return rows[0][0].replace('#:guid:', '') return ''
def abgabepreis_kunde(artnr, kundennr, auftragsdatum=None): """ Verkaufspreis für einen Artikel in Abhängigkeit von kundennr und Auftragsdatum ermitteln. Höchste Priorität hat der für einen Kunden hinterlegt Preis. Zweithöchste Priorität hat der für die Preisliste (Kundengruppe) hinterlegte Preis Niedrigste Priorität hat der Listenpreis aus den Artikelstammdaten. Rückgabe ist tuple mit Preis und Herkunft des Preises. >>> abgabepreis_kunde('04711', 99954) (1500, 'Preisliste 95') >>> abgabepreis_kunde('04711', 98000) (1400, 'Listenpreis') >>> abgabepreis_kunde('04711', 94763) (1300, 'Kundenpreis') """ if not auftragsdatum: auftragsdatum = datetime.date.today() # Kundennr als Zeichenkette kundennr = remove_prefix(kundennr, 'SC') date_str = sql_quote(date2softm(auftragsdatum)) # 1. Preis für Kunde hinterlegt? conditions = ["PNSANR=PRSANR", "PRANW='A'", "PRSTAT=' '", "PNSTAT=' '", "PRARTN=%s" % sql_quote(artnr), "PRDTBI>=%s" % date_str, "PRDTVO<=%s" % date_str, ] condition_kunde = conditions + ["PRKDNR=%s" % pad('PRKDNR', kundennr)] rows = query(['XPN00', 'XPR00'], fields=['PNPRB'], condition=' AND '.join(condition_kunde), ordering='PRDTVO', limit=1) if rows: return (int(rows[0][0] * 100), 'Kundenpreis') # 2. Preis aus Preislistennr. des Kunden ermitteln condition_gruppe = conditions + [ # "PRPRLK = %s" % sql_quote(kunde['kunden_gruppe']), "KDKDNR=%s" % pad('KDKDNR', kundennr), "PRPRLK=KDKGRP" ] rows = query(['XPN00', 'XPR00', 'XKD00'], fields=['PNPRB', 'PRPRLK'], ordering='PRDTVO', condition=' AND '.join(condition_gruppe), limit=1) if rows: return (int(rows[0]['preis'] * 100), 'Preisliste %s' % rows[0]['preisliste_kunde']) # 3. Listenpreis aus Artikelstammdaten return (listenpreis(artnr), 'Listenpreis')
def get_auftrag_by_auftragsnr(auftragsnr, header_only=False): """Auftrag mit Auftragsnummer auftragsnr zurueckgeben""" auftragsnr = remove_prefix(auftragsnr, 'SO') auftraege = _auftraege(["AKAUFN=%s" % sql_escape(auftragsnr)], header_only=header_only) if len(auftraege) > 1: raise RuntimeError("Mehr als ein Auftrag mit auftragsnr %s vorhanden" % auftragsnr) if not auftraege: return None return auftraege[0]
def fertige_auftraege_kunde(kundennr, limit=None, header_only=False, canceled=False): """Alle abgeschlossene Aufträge für eine Kundennummer ermitteln. Gibt eine Liste von dict()s zurück. Wenn `canceled == False` werden keine stornierten Aufträge und Positionen zurückgegeben.""" kundennr = remove_prefix(kundennr, 'SC') # dies mag stornierte Aufträge übersehen - hab ich bisher nicht überprüft auftraege = _auftraege(["AKKDNR=%s" % pad('AKKDNR', kundennr), "(AKKZVA=1 OR AKSTAT='X')"], limit=limit, header_only=header_only, canceled=canceled) return auftraege
def get_auftrag_by_auftragsnr(auftragsnr, header_only=False, canceled=False): """Auftrag mit Auftragsnummer auftragsnr zurueckgeben. Wenn `canceled == False` werden keine stornierten Aufträge und Positionen zurückgegeben.""" auftragsnr = remove_prefix(auftragsnr, 'SO') auftraege = _auftraege(["AKAUFN=%s" % sql_escape(auftragsnr)], header_only=header_only, canceled=canceled) if len(auftraege) > 1: raise RuntimeError("Mehr als ein Auftrag mit auftragsnr %s vorhanden" % auftragsnr) if not auftraege: return None return auftraege[0]
def get_rahmenauftraege(kundennr, artnr): """Gib die Auftragsnummern aller offenen Rahmenaufträge für den Kunden und den Artikel als Liste zurück. >>> get_rahmenauftraege('SC66663', '14600') ['SO1205711'] """ kundennr = remove_prefix(kundennr, 'SC') conditions = ["AKAUFN=APAUFN", "AKKZVA=0", "APKZVA=0", "AKSTAT<>'X'", "APSTAT<>'X'", "AKAUFN>0", "AKAUFA='R'", "AKKDNR=%s" % pad('AKKDNR', kundennr), "APARTN=%s" % sql_quote(artnr)] rows = query(['AAK00', 'AAP00'], condition=' AND '.join(conditions), fields=['AKAUFN'], ua='husoftm2.auftraege.get_rahmenauftraege') return [add_prefix(row[0], 'SO') for row in rows]
def auftraege_kunde(kundennr, limit=None, header_only=False, canceled=False): """Alle Aufträge für eine Kundennummer ermitteln. Gibt eine Liste von dict()s zurück. Wenn `canceled == False` werden keine stornierten Aufträge und Positionen zurückgegeben.""" conditions = [] if '.' in kundennr: kundennr, adressindex = kundennr.split('.') conditions.append("AKVANR=%d" % int(adressindex)) kundennr = remove_prefix(kundennr, 'SC') conditions.append("AKKDNR=%s" % pad('AKKDNR', kundennr)) auftraege = _auftraege(conditions, limit=limit, header_only=header_only, canceled=canceled) return auftraege
def get_address(lieferantennr): """Liefert die Adresse zu einer Lieferantennummer.""" # Präfix für Lieferanten (SP) entfernen. Für Präfix siehe # https://sites.google.com/a/hudora.de/intern/it-administration/nummern/nummernkreise lieferantennr = remove_prefix(lieferantennr, 'SP') # LISTAT kann 'X' (gelöscht) oder 'I' (inaktiv) sein. Wir wollen nur gültige Adressen, also LISTAT = ' ' rows = query(tables=['XLI00'], condition="LISTAT=' ' AND LIKZLI=1 AND LILINR=%s" % pad('LILINR', lieferantennr), limit=1, ua='husoftm2.lieferanten') if rows: row = rows[0] row['lieferantennr'] = add_prefix(row['lieferantennr'], 'SP') row['land'] = land2iso(row.pop('laenderkennzeichen')) return row return None
def get_lieferschein(lieferscheinnr, header_only=False): """Gibt ein Lieferscheindict für eine Lieferscheinnummer zurück""" lieferscheinnr = remove_prefix(lieferscheinnr, 'SL') lscheine = _lieferscheine(["LKLFSN = %s" % sql_quote(lieferscheinnr)], limit=1, header_only=header_only) if lscheine: if len(lscheine) > 1: raise RuntimeError('Suche nach %s hat mehr als einen Lieferschein ergeben: %r' % (lieferscheinnr, lscheine)) lschein = lscheine[0] infotext = lschein.get('infotext_kunde') if infotext and isinstance(infotext, list): lschein['infotext_kunde'] = ', '.join(infotext) if not lschein.get('datum'): raise RuntimeError('LS %s hat kein Datum: %r' % (lieferscheinnr, lschein)) return lschein return {}
def txt_auslesen(auftragsnrs, postexte=None, kopftexte=None, kopfdaten=None, posdaten=None): """Gibt Positions und Kopftexte für eine Liste von Auftragsnummern zurück.""" # Die Clients können dicts mit vorbelegten Daten mitgeben. Das ist vor allem da nützlich, wo # Jobs relativ viele Auftruage in Batches abarbeiten. postexte = postexte or {} posdaten = posdaten or {} kopftexte = kopftexte or {} kopfdaten = kopfdaten or {} # Wir arbeiten auf einer Kopie der Auftragsnummern, da wir von der Liste batches abschneiden. allauftrnr = auftragsnrs[:] while allauftrnr: # In 50er Schritten Texte lesen, 'SO'-Kürzel entfernen. # Wir erwarten auf jeden Fall String-Parameter. batch = [remove_prefix(x, 'SO') for x in allauftrnr[:50]] allauftrnr = allauftrnr[50:] # Texte aus SoftM einlesen condition = 'ATAUFN IN (%s)' % ','.join((str(x) for x in batch)) for row in query(['AAT00'], ordering=['ATTART', 'ATLFNR'], condition=condition, ua='husoftm2.texte'): # Jeden der eingelesenen Texte nach Textart klassifizieren. row['textart'] = int(row['textart']) auftragsnr = "SO%s" % remove_prefix(row['auftragsnr'], 'SO') # Textzeilen die leer sind oder nur Trennzeichen enthalten, ignorieren wir. if not row['text'].strip('=*_- '): continue # Wir behandeln hier nur Texte, die auf Auftragsbestätigungen, Lieferscheinen oder Rechnungen # auftauchen sollen. Allerdings drucken wir diese dann auch auf beiden Belegarten auf - keine # weiteren Unterscheidungen. if row['andruck_re'] or row['andruck_ls'] or row['andruck_ab']: if row['andruck_re'] > 1 or row['andruck_ls'] > 3: raise NotImplementedError(row) # Wir haben gelegentlich Texte mit `andruck_ab == 2` die offensichtlich nicht als # Kundenbelege sollen. Der Wert 2 in diesem Feld ist gänzlich undokumentiert, # wir ignorieren bis auf weiteres einfach diese Zeilen. if row['andruck_ab'] > 1: continue # Texte wo andruck_ls=2 steht soll man laut SoftM "nur auf KB drucken". # Die Inhalte sind manchmal grenzwertig ... an dieser stelle kann man die aussortieren, # machen wir aber zur zeit nicht. if row['andruck_ls'] == 1 and (not row['andruck_re']) and (not row['andruck_ab']): pass # Die Statistische Warennummer wird als Positionstext mit der Nummer 5 transportiert # In Produktivdaten haben wir die aber bisher noch nicht gesehen. if row['textart'] == 5: postexte.setdefault(auftragsnr, {} ).setdefault(row['auftragsposition'], [] ).append("Statistische Warennummer: %s" % row['text'].strip()) # Die Textarten 2, 7 und 8 sind verschiedenen Positionstexte: # * 2 Abweichende Artikelbezeichnung # * 7 Auftragstexte vor Position # * 8 Auftragstexte nach Position # Wir fassen alle drei Textarten in einem einzigen Feld zusammen elif row['auftragsposition'] > 0 and row['textart'] in (2, 7, 8): postexte.setdefault(auftragsnr, {} ).setdefault(row['auftragsposition'], [] ).append(row['text'].strip()) # Textart 7 bei Position 0 ist eine Faxnummer. Warum auch immer. Wir ignorieren das. # Das Feld ist **sehr oft** gefüllt. elif row['auftragsposition'] == 0 and row['textart'] == 7: pass # Bei Position 0 sind Textart 8 und 9 Fuß- und Kopftexte für den gesammten Auftrag. # Wir fassen die in einem einzigen Feld zusammen. elif row['auftragsposition'] == 0 and row['textart'] in (8, 9): kopftexte.setdefault(auftragsnr, []).append(row['text']) else: # Andere Textarten sind uns bisher nicht untergekommen. print row raise NotImplementedError # Wenn der Text eigentlich nirgends angedruckt werden soll, dann ist es entweder ein Warntext # bei der Auftragserfassung, oder ein Verschlüsseltes Datenfeld. else: if int(row['auftragsposition'] == 0): # Erfassungstexte sind Texte, die bei der Auftragserfassung angeziegt werden, aber auf # keinem Beleg erscheinen (kein druckkennzeichen) - die ignorieren wir hier. # Die gesonderten Datenfelder, die mit #:VARNAME: beginnen, verwenden wir aber weiter _erfassungstexte, daten = texte_trennen([row['text']]) if daten: kopfdaten.setdefault(auftragsnr, {}).update(daten) else: # row['auftragsposition'] > 0: erfassungstexte, daten = texte_trennen([row['text']]) if daten: posdaten.setdefault(auftragsnr, {} ).setdefault(row['auftragsposition'], {} ).update(daten) return postexte, kopftexte, posdaten, kopfdaten
def auftraege_kunde(kundennr, limit=None, header_only=False): """Alle Aufträge für eine Kundennummer ermitteln. Gibt eine Liste von dict()s zurück.""" kundennr = remove_prefix(kundennr, 'SC') auftraege = _auftraege(["AKKDNR=%s" % pad('AKKDNR', kundennr)], limit=limit, header_only=header_only) return auftraege
def _auftraege(additional_conditions=None, addtables=None, mindate=None, maxdate=None, limit=None, header_only=False): """ Alle Aufträge ermitteln `additional_conditions` kann eine Liste von SQL-Bedingungen enthalten, die die Auftragssuche einschränken. `mindate` & `maxdate` können den Anliefertermin einschränken. `limit` kann die Zahl der zurückgelieferten Aufträge einschraenken. Dabei werden groessere Auftragsnummern zuerst zurueck gegeben. Rückgabewert sind dicts nach dem Lieferungprotokoll. Wenn header_only == True, werden nur Auftragsköpfe zurück gegeben, was deutlich schneller ist. """ conditions = ["AKSTAT<>'X'"] if mindate and maxdate: conditions.append("AKDTER BETWEEN %s AND %s" % (date2softm(mindate), date2softm(maxdate))) elif mindate: conditions.append("AKDTER > %s" % date2softm(mindate)) elif maxdate: conditions.append("AKDTER < %s" % date2softm(maxdate)) if additional_conditions: conditions.extend(additional_conditions) condition = " AND ".join(conditions) koepfe = {} kopftexte = {} if addtables is None: addtables = [] # Köpfe und Adressen einlesen for kopf in query(['AAK00'] + addtables, ordering=['AKAUFN DESC'], condition=condition, joins=[('XKD00', 'AKKDNR', 'KDKDNR')], limit=limit, ua='husoftm2.auftraege'): d = dict(kundennr="SC%s" % kopf['kundennr_warenempf'], auftragsnr="SO%s" % kopf['auftragsnr'], auftragsnr_kunde=kopf['auftragsnr_kunde'], erfassung=kopf['AAK_erfassung_date'], aenderung=kopf['AAK_aenderung_date'], sachbearbeiter=husoftm2.sachbearbeiter.resolve(kopf['sachbearbeiter']), anliefertermin=kopf['liefer_date'], teillieferung_erlaubt=(kopf['teillieferung_erlaubt'] == 1), erledigt=(kopf['voll_ausgeliefert'] == 1), positionen=[], # * *auftragsnr_kunde* - id of the order submitted by the customer # * *info_kunde* - Freitext der für den Empfänger relevanz hat ) koepfe[kopf['auftragsnr']] = d if header_only: return koepfe.values() allauftrnr = koepfe.keys() # Texte auslesen # Die dritte und vierte Position des Werts von txt_auslesen sind posdaten und kopfdaten. # Es handelt sich dabei wohl um Texte, die nicht angedruckt werden sollen. # Bis auf weiteres werden diese hier ignoriert. postexte, kopftexte, _, _ = txt_auslesen(allauftrnr) while allauftrnr: # In 50er Schritten Auftragspositionen lesen und den 50 Aufträgen zuordnen batch = allauftrnr[:50] allauftrnr = allauftrnr[50:] # Abweichende Lieferadressen for row in query(['XAD00'], ua='husoftm2.lieferscheine', condition="ADAART=1 AND ADRGNR IN (%s)" % ','.join([str(x) for x in batch])): koepfe[row['nr']]['lieferadresse'] = dict(name1=kopf['name1'], name2=kopf['name2'], name3=kopf['name3'], strasse=row['strasse'], land=husoftm2.tools.land2iso(row['laenderkennzeichen']), plz=row['plz'], ort=row['ort']) # Positionen einlesen for row in query(['AAP00'], condition="APSTAT<>'X' AND APAUFN IN (%s)" % ','.join([str(x) for x in batch]), ua='husoftm2.auftraege'): d = dict(menge=int(row['bestellmenge']), artnr=row['artnr'], liefer_date=row['liefer_date'], menge_offen=int(row['menge_offen']), fakturierte_menge=int(row['fakturierte_menge']), erledigt=(row['voll_ausgeliefert'] == 1), # 'position': 2, # 'teilzuteilungsverbot': u'0', ) texte = postexte.get(row['auftragsnr'], {}).get(row['position'], []) texte, attrs = texte_trennen(texte) d['infotext_kunde'] = texte if 'guid' in attrs: d['guid'] = attrs['guid'] koepfe[row['auftragsnr']]['positionen'].append(d) # Kopftexte zuordnen for auftragsnr, texte in kopftexte.items(): texte, attrs = texte_trennen(texte) koepfe[remove_prefix(auftragsnr, 'SO')]['infotext_kunde'] = texte if 'guid' in attrs: koepfe[remove_prefix(auftragsnr, 'SO')]['guid'] = attrs['guid'] return koepfe.values()
def get_ls_kb_data(conditions, additional_conditions=None, limit=None, header_only=False, is_lieferschein=True): """Lieferscheindaten oder Kommsissionierbelegdaten entsprechend dem Lieferungsprotokoll. Wenn is_lieferschein = False, dann werden Kommiauftragsdaten zurückgebeben (Kommimengen) """ cachingtime = 60 * 60 * 12 if additional_conditions: conditions.extend(additional_conditions) condition = " AND ".join(conditions) koepfe = {} auftragsnr2satznr = {} satznr2auftragsnr = {} # Lieferscheinkopf JOIN Kundenadresse um die Anzahl der Queries zu minimieren # JOIN Lieferadresse geht nicht, weil wir "ADAART=1" mit DB2/400 nicht klappt for row in query(['ALK00'], ordering=['LKSANK DESC'], condition=condition, limit=limit, joins=[('XKD00', 'LKKDNR', 'KDKDNR'), ('AAK00', 'LKAUFS', 'AKAUFN')], cachingtime=cachingtime, ua='husoftm2.lieferscheine'): kopf = dict(positionen=[], auftragsnr="SO%s" % row['auftragsnr'], auftragsnr_kunde=row['auftragsnr_kunde'], erfassung=row['ALK_erfassung'], aenderung=row.get('ALK_aenderung'), anliefer_date=row['anliefer_date'], kundennr="SC%s" % row['rechnungsempfaenger'], lieferadresse=dict(kundennr="SC%s" % row['warenempfaenger']), anlieferdatum=row['anliefer_date'], lager="LG%03d" % int(row['lager']), kommiauftragnr="KA%s" % row['kommibelegnr'], kommiauftrag_datum=row['kommibeleg_date'], lieferscheinnr="SL%s" % row['lieferscheinnr'], name1=row.get('name1', ''), name2=row.get('name2', ''), name3=row.get('name3', ''), strasse=row.get('strasse', ''), land=husoftm2.tools.land2iso(row['laenderkennzeichen']), plz=row.get('plz', ''), ort=row.get('ort', ''), tel=row.get('tel', ''), fax=row.get('fax', ''), art=row.get('art', ''), softm_created_at=row.get('ALK_lieferschein'), ) # Wir hatten erhebliche Probleme, weil datumsfelder mal befüllt waren # und mal nicht (race condition) - wir gehen deswegen recht großzügig # bei der suceh nach einem geeigenten feld vor. for fieldname in ['ALK_lieferschein', 'ALK_aenderung']: if row.get(fieldname): kopf['datum'] = row.get(fieldname) break if not kopf.get('datum'): raise RuntimeError("Konnte kein Datum ermitteln %r", row) pos_key = remove_prefix((row['satznr']), 'SO') if row.get('bezogener_kopf'): pos_key = remove_prefix(row['bezogener_kopf'], 'SO') auftragsnr2satznr[remove_prefix(row['auftragsnr'], 'SO')] = pos_key satznr2auftragsnr[pos_key] = remove_prefix(row['auftragsnr'], 'SO') if row['ALK00_dfsl']: # Basis dieses Codes: # [LH #721] LS mit 0-mengen vermeiden # Wir sehen im Produktiv-Betrieb immer wieder Lieferscheine mit der Menge "0" # erzeugt werden. Wir vermuten hier eine race Condition, bei der die # ALK00 schon mit der Lieferscheinnummer geupdated ist, die ALN00 aber noch # nicht mit der Lieferscheinmenge. # Eine weitere Vermutung ist, dass wenn in der ALN00 die Menge noch cniht eingetragen # ist, dort auch noch die Lieferscheinnummer fehlt. Das versuchen wir hier abzufangen. # Lieber ein Crash, als ein Lieferschein mit (unbegründerter) 0-menge. raise RuntimeError("Dateiführungsschlüssel in ALK00: %r" % kopf) koepfe[pos_key] = kopf if header_only: return koepfe.values() satznr = koepfe.keys() allauftrnr = koepfe.keys() # Alle texte einlesen postexte, kopftexte, posdaten, kopfdaten = txt_auslesen([satznr2auftragsnr[x] for x in allauftrnr]) while satznr: # In 50er Schritten Auftragspositionen & Texte lesen und den 50 Aufträgen zuordnen batch = satznr[:50] satznr = satznr[50:] # Abweichende Lieferadressen condition = "ADAART=1 AND ADRGNR IN (%s) AND ADRGNR=AKAUFN" % ','.join([str(satznr2auftragsnr[x]) for x in batch]) for row in query(['XAD00', 'AAK00'], cachingtime=cachingtime, ua='husoftm2.lieferscheine', condition=condition): aktsatznr = auftragsnr2satznr[row['nr']] koepfe[aktsatznr]['lieferadresse'].update(dict(name1=row['name1'], name2=row['name2'], name3=row['name3'], strasse=row['strasse'], land=husoftm2.tools.land2iso(row['laenderkennzeichen']), plz=row['plz'], ort=row['ort'])) versandadressnr = row['versandadressnr'] warenempfaenger = koepfe[aktsatznr]['lieferadresse']['kundennr'] if versandadressnr: warenempfaenger = "%s.%03d" % (warenempfaenger, versandadressnr) koepfe[aktsatznr]['lieferadresse']['warenempfaenger'] = warenempfaenger # Positionen & Positionstexte zuordnen for row in query(['ALN00'], condition="LNSTAT<>'X' AND LNSANK IN (%s)" % ','.join([str(x) for x in batch]), cachingtime=cachingtime, ua='husoftm2.lieferscheine'): if is_lieferschein == True: lsmenge = int(row['menge']) if row['ALN00_dfsl']: # Basis dieses Codes: # [LH #721] LS mit 0-mengen vermeiden # Wir sehen im Produktiv-Betrieb immer wieder Lieferscheine mit der Menge "0" # erzeugt werden. Wir vermuten hier eine race Condition, bei der die # ALK00 schon mit der Lieferscheinnummer geupdated ist, die ALN00 aber noch # nicht mit der Lieferscheinmenge. # Eine weitere Vermutung ist, dass wenn in der ALN00 die Menge noch cniht eingetragen # ist, dort auch noch die Lieferscheinnummer fehlt. Das versuchen wir hier abzufangen. # Lieber ein Crash, als ein Lieferschein mit (unbegründerter) 0-menge. raise RuntimeError("Dateiführungsschlüssel in ALN00: %r" % row) else: lsmenge = int(row['menge_komissionierbeleg']) pos = dict(artnr=row['artnr'], guid='%s-%03d-%03d' % (row['kommibelegnr'], row['auftrags_position'], row['kommibeleg_position']), menge=lsmenge) texte = postexte.get(remove_prefix(row['auftragsnr'], 'SO'), {}).get(row['auftrags_position'], []) pos['infotext_kunde'] = texte if 'guid' in posdaten.get(remove_prefix(row['auftragsnr'], 'SO'), {}): pos['auftragpos_guid'] = posdaten.get(remove_prefix(row['auftragsnr'], 'SO'), {}).get('guid') else: pos['auftragpos_guid'] = "%s-%03d" % (row['auftragsnr'], row['auftrags_position']) lieferung = koepfe[remove_prefix(row['satznr_kopf'], 'SO')] lieferung['positionen'].append(pos) # *Sachbearbeiter* ist der, der den Vorgang tatsächlich bearbeitet hat. *Betreuer* ist # die (oder der), die für den Kunden zusändig ist. lieferung['sachbearbeiter'] = husoftm2.sachbearbeiter.resolve(row['sachbearbeiter_bearbeitung']) # Kopftexte zuordnen for auftragsnr, texte in kopftexte.items(): pos_key = auftragsnr2satznr[remove_prefix(auftragsnr, 'SO')] koepfe[pos_key]['infotext_kunde'] = texte for auftragsnr, werte in kopfdaten.items(): if 'guid' in werte: pos_key = auftragsnr2satznr[remove_prefix(auftragsnr, 'SO')] koepfe[pos_key]['guid_auftrag'] = werte['guid'] for aktsatznr in koepfe.keys(): # Entfernt Konstrukte wie das: # "kundennr": "SC19971", # "lieferadresse": { # "kundennr": "SC19971" # } if len(koepfe[aktsatznr]['lieferadresse']) == 1: if koepfe[aktsatznr]['lieferadresse']['kundennr'] == koepfe[aktsatznr]['kundennr']: del(koepfe[aktsatznr]['lieferadresse']) return koepfe.values()
def abgabepreise_kunde(artnrs, kundennr, auftragsdatum=None): """ Verkaufspreis für einen oder mehrere Artikel in Abhängigkeit von kundennr und Auftragsdatum ermitteln. Der Rückgabewert ist ein dict mit den ArtNr. als Schlüssel. Die Werte sind Preisinformationen als Tupel (Preis, Herkunft) oder None, falls zu der ArtNr. kein Preis ermittelt werden konnte. Die Logik funktioniert genau wie bei abgabepreis_kunde: Es werden zuerst kundenspezifische Preise, dann kundengruppen-spezifische Preise und als letztes Listenpreise ermittelt. """ warnings.warn("use `cs.salesforce.preise` instead!", DeprecationWarning, stacklevel=2) if not auftragsdatum: auftragsdatum = datetime.date.today() artnrs = set(artnrs) kundennr = remove_prefix(kundennr, 'SC') date_str = sql_quote(date2softm(auftragsdatum)) abgabepreise = {} # 1. Preise für Kunden hinterlegt? conditions = ["PNSANR=PRSANR", "PRANW='A'", "PRPRLK<>''" "PRSTAT=' '", "PNSTAT=' '", "PRDTBI>=%s" % date_str, "PRDTVO<=%s" % date_str, ] condition_kunde = conditions + ["PRKDNR=%s" % sql_quote("%8s" % kundennr), "PRARTN IN (%s)" % ",".join([sql_quote(artnr) for artnr in artnrs])] rows = query(tables=['XPN00', 'XPR00'], fields=['PRARTN', 'PNPRB'], condition=' AND '.join(condition_kunde), ordering='PRDTVO') for row in rows: if row['artnr'] in artnrs: artnrs.remove(row['artnr']) abgabepreise[row['artnr']] = (int(row['preis'] * 100), u'Kundenpreis') if not artnrs: return abgabepreise # 2. Preise aus Preislistennr. des Kunden ermitteln condition_gruppe = conditions + [ # "PRPRLK = %s" % sql_quote(kunde['kunden_gruppe']), "PRARTN IN (%s)" % ",".join([sql_quote(artnr) for artnr in artnrs]), "KZKDNR=%s" % pad('KZKDNR', kundennr), "PRPRLK=KZPREL" ] rows = query(tables=['XPN00', 'XPR00', 'AKZ00'], fields=['PRARTN', 'PNPRB', 'PRPRLK'], condition=' AND '.join(condition_gruppe), ordering='PRDTVO') for row in rows: if row['artnr'] in artnrs: artnrs.remove(row['artnr']) abgabepreise[row['artnr']] = (int(row['preis'] * 100), u'Preisliste %s' % row['preisliste_kunde']) if not artnrs: return abgabepreise # 3. Listenpreis aus Artikelstammdaten for artnr, preis in listenpreise(artnrs).iteritems(): if artnr in artnrs: artnrs.remove(artnr) abgabepreise[artnr] = (preis, u'Listenpreis') for artnr in artnrs: abgabepreise[artnr] = None return abgabepreise
def _auftraege(additional_conditions=None, addtables=None, mindate=None, maxdate=None, limit=None, header_only=False, canceled=False): """ Alle Aufträge ermitteln `additional_conditions` kann eine Liste von SQL-Bedingungen enthalten, die die Auftragssuche einschränken. `mindate` & `maxdate` können den Anliefertermin einschränken. `limit` kann die Zahl der zurückgelieferten Aufträge einschraenken. Dabei werden groessere Auftragsnummern zuerst zurueck gegeben. `header_only` ruft nur Auftragsköpfe ab und ist bedeutend schneller `canceled` wenn True, werden auch stornierte Aufträge zurück gegeben. Rückgabewert sind dicts nach dem Lieferungprotokoll. Wenn header_only == True, werden nur Auftragsköpfe zurück gegeben, was deutlich schneller ist. """ # Solange der Client das nciht gesondert verlangt, werden stornierte Aufträge ignoriert. if not canceled: conditions = ["AKSTAT<>'X'"] else: conditions = [] # Anliefertermin ist ein Range if mindate and maxdate: conditions.append("AKDTER BETWEEN %s AND %s" % (date2softm(mindate), date2softm(maxdate))) # Anliefertermin ist nach unten begrenzt elif mindate: conditions.append("AKDTER > %s" % date2softm(mindate)) # Anliefertermin ist nach oben begrenzt elif maxdate: conditions.append("AKDTER < %s" % date2softm(maxdate)) # vom Aufrufer direkt angegebenen, weitere SQL Bedingungen zufügen. Diese werden mit `AND` verkettet. if additional_conditions: conditions.extend(additional_conditions) condition = " AND ".join(conditions) koepfe = {} kopftexte = {} auftragsnr_to_lieferadresse_kdnr = {} if addtables is None: addtables = [] # Köpfe und Adressen einlesen for kopf in query(['AAK00'] + addtables, ordering=['AKAUFN DESC'], condition=condition, joins=[('XKD00', 'AKKDNR', 'KDKDNR')], limit=limit, ua='husoftm2.auftraege'): d = dict(kundennr="SC%s" % kopf['kundennr_warenempf'], kundennr_rechnung="SC%s" % kopf['kundennr_rechnungsempf'], name1=kopf['name1'], name2=kopf['name2'], name3=kopf['name3'], strasse=kopf['strasse'], land=husoftm2.tools.land2iso(kopf['laenderkennzeichen']), plz=kopf['plz'], ort=kopf['ort'], auftragsnr="SO%s" % kopf['auftragsnr'], auftragsnr_kunde=kopf['auftragsnr_kunde'], erfassung=kopf.get('AAK_erfassung'), aenderung=kopf.get('AAK_aenderung', None), sachbearbeiter=husoftm2.sachbearbeiter.resolve(kopf['sachbearbeiter']), anliefertermin=kopf['liefer_date'], teillieferung_erlaubt=(kopf['teillieferung_erlaubt'] == 1), # TODO: md: ich denke, "erledigt" ist ein Auftrag auch, wenn er storneirt wurde, # oder wenn alle Positionen auf voll_ausgeliefert stehen. erledigt=(kopf['voll_ausgeliefert'] == 1), positionen=[], art=kopf['art'], storniert=(kopf['AAK_status'] == 'X')) koepfe[kopf['auftragsnr']] = d # Auftrag geht an die 'normale' Lieferadresse: Kein .\d\d\d-Suffix an die `lieferadresse.kundennr` if kopf['versandadressnr'] == 0: auftragsnr_to_lieferadresse_kdnr[kopf['auftragsnr']] = add_prefix(kopf['kundennr_warenempf'], 'SC') # Auftrag geht an eine abweichende Lieferadresse: .00?-Suffix an die `lieferadresse.kundennr` hängen. else: lieferadresse_kdnr = add_prefix("%s.%03d" % (kopf['kundennr_warenempf'], kopf['versandadressnr']), "SC") auftragsnr_to_lieferadresse_kdnr[kopf['auftragsnr']] = lieferadresse_kdnr if header_only: return koepfe.values() allauftrnr = koepfe.keys() # Texte auslesen # Die dritte und vierte Position des Werts von txt_auslesen sind posdaten und kopfdaten. # Es handelt sich dabei wohl um Texte, die nicht angedruckt werden sollen. # Bis auf weiteres werden diese hier ignoriert. postexte, kopftexte, _, _ = txt_auslesen(allauftrnr) while allauftrnr: # In 50er Schritten Auftragspositionen lesen und den 50 Aufträgen zuordnen batch = allauftrnr[:50] allauftrnr = allauftrnr[50:] # Abweichende Lieferadressen for row in query(['XAD00'], ua='husoftm2.auftraege', condition="ADAART=1 AND ADRGNR IN (%s)" % ','.join([str(x) for x in batch])): koepfe[row['nr']]['lieferadresse'] = dict(name1=row['name1'], name2=row['name2'], name3=row['name3'], strasse=row['strasse'], land=husoftm2.tools.land2iso(row['laenderkennzeichen']), plz=row['plz'], kundennr=auftragsnr_to_lieferadresse_kdnr[row['nr']], ort=row['ort']) # Positionen einlesen if not canceled: poscondition = "APSTAT<>'X' AND APAUFN IN (%s)" % ','.join([str(x) for x in batch]) else: poscondition = "APAUFN IN (%s)" % ','.join([str(x) for x in batch]) for row in query(['AAP00'], condition=poscondition, ua='husoftm2.auftraege'): d = dict(menge=int(row['bestellmenge']), lager="LG%s" % row['lager'], artnr=row['artnr'], liefer_date=row['liefer_date'], menge_offen=int(row['menge_offen']), fakturierte_menge=int(row['fakturierte_menge']), erledigt=(row['voll_ausgeliefert'] == 1), storniert=(row['AAP_status'] == 'X'), posnr=int(row['position']), _aenderung=row.get('AAP_aenderung'), _erfassung=row.get('AAP_erfassung'), _zuteilung=row.get('AAP_zuteilung'), # 'position': 2, # 'teilzuteilungsverbot': u'0', ) # Preis einfügen if row.get('verkaufspreis'): d['preis'] = row['verkaufspreis'] texte = postexte.get(row['auftragsnr'], {}).get(row['position'], []) texte, attrs = texte_trennen(texte) d['infotext_kunde'] = texte if 'guid' in attrs: d['guid'] = attrs['guid'] koepfe[row['auftragsnr']]['positionen'].append(d) # Kopftexte zuordnen for auftragsnr, texte in kopftexte.items(): texte, attrs = texte_trennen(texte) koepfe[remove_prefix(auftragsnr, 'SO')]['infotext_kunde'] = texte if 'guid' in attrs: koepfe[remove_prefix(auftragsnr, 'SO')]['guid'] = attrs['guid'] return koepfe.values()
def lieferscheine_auftrag(auftragsnr, header_only=False): """Gibt eine Liste mit Lieferscheindicts für einen Auftrag zurück""" auftragsnr = remove_prefix(auftragsnr, 'SO') return _lieferscheine(["LKAUFS = %s" % sql_quote(auftragsnr)], header_only=header_only)