Repository: hudora/pyShipping Branch: master Commit: 089c502db5d7 Files: 36 Total size: 310.0 KB Directory structure: gitextract_q2tr1ik7/ ├── .gitignore ├── CHANGES ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── pyshipping/ │ ├── 3dbpp.c │ ├── __init__.py │ ├── addressvalidation.py │ ├── binpack.py │ ├── binpack_simple.py │ ├── carriers/ │ │ ├── __init__.py │ │ └── dpd/ │ │ ├── __init__.py │ │ ├── georoute.py │ │ ├── georoute_test.py │ │ └── georoutetables/ │ │ ├── COUNTRY │ │ ├── LOCATION.DE │ │ ├── LOCATION.EN │ │ ├── LOCATION.FR │ │ ├── SERVICE │ │ ├── SERVICEINFO.CS │ │ ├── SERVICEINFO.CZ │ │ ├── SERVICEINFO.DE │ │ ├── SERVICEINFO.EE │ │ └── SERVICEINFO.EN │ ├── fortras/ │ │ ├── __init__.py │ │ ├── bordero.py │ │ ├── entl.py │ │ ├── fakt.py │ │ ├── fortras_stat.py │ │ └── test.py │ ├── package.py │ └── shipment.py ├── requirements.txt ├── setup.py └── testdata.txt ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.pyc *.pyo *.so *~ build dist html pyShipping.egg-info *.bak ================================================ FILE: CHANGES ================================================ 1.9: neue Routeninfos von DPD zum 2011-05-07 1.8: neue Routeninfos von DPD zum 2011-01-03 1.7: neue Routeninfos von DPD zum 6.9.2010 1.6: addressvalidation added 1.5: Keep Package dimensions sorted, buendelung(), __add__() and hat_gleiche_seiten() added binpacking (c) David Pisinger, Silvano Martello, Daniele Vigo and an independent pure Python implementation unified PackageSize and Package, made Package sortable 1.4: Gemeinsame Basisklasse fuer Exceptions in pyshipping.carriers.dpd.georoute 1.3p3: Added new Routing Table for DPD (Routendatenbank Januar - April 2010) 1.3p2: Added new Routing Table for DPD 1.3p1: Provide extra argument basedir to fortras.bordero.ship(). Bordero files will be saved in this directory. ================================================ FILE: LICENSE ================================================ Original code in this disturibution is Copyright 2008, 2009 HUDORA GmbH. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY HUDORA GmbH ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. This distribution contains software by third parties which might be licensed differently. See the respective sourcefiles for details. ================================================ FILE: MANIFEST.in ================================================ include README.rst include pyshipping/carriers/dpd/georoutetables/* ================================================ FILE: Makefile ================================================ # setting the PATH seems only to work in GNUmake not in BSDmake PATH := ./testenv/bin:$(PATH) check: pep8 -r --ignore=E501 pyshipping/ sh -c 'PYTHONPATH=. pyflakes pyshipping/' -sh -c 'PYTHONPATH=. pylint -iy --max-line-length=110 pyshipping/' # -rn build: python setup.py build test: PYTHONPATH=. python pyshipping/__init__.py # find import errors PYTHONPATH=. python pyshipping/shipment.py PYTHONPATH=. python pyshipping/package.py PYTHONPATH=. python pyshipping/fortras/test.py PYTHONPATH=. python pyshipping/binpack.py # These tests tend to fail because of routing table updates PYTHONPATH=. python pyshipping/carriers/dpd/georoute_test.py dependencies: virtualenv testenv pip -q install -E testenv -r requirements.txt statistics: sloccount --wide --details pyshipping | tee sloccount.sc upload: build doc python setup.py sdist upload doc: build rm -Rf html mkdir -p html mkdir -p html/fortras mkdir -p html/carriers sh -c '(cd html; pydoc -w ../pyshipping/*.py)' sh -c '(cd html/fortras; pydoc -w ../../pyshipping/*.py)' sh -c '(cd html/carriers; pydoc -w ../../pyshipping/*.py)' install: build sudo python setup.py install clean: rm -Rf testenv build dist html test.db pyShipping.egg-info pylint.out sloccount.sc pip-log.txt find . -name '*.pyc' -or -name '*.pyo' -delete .PHONY: test build clean check upload doc install ================================================ FILE: README.rst ================================================ pyShipping provides connections to interface with shipping companies and to transport shipping related information. * package - shipping/cargo related calculations based on a unit of shipping (box, crate, package), includes a bin packing implementation in pure Python * sendung - defines an abstract shippment (Sendung), with packages and calculations based on that * addressvalidation - check if an address is valid * carriers.dpd - calculation of DPD/Georoutes routing data and labels. Included tables are for shippments from Wuppertal but it should work with all other german routing tables. See this Blogpost_ about updating routing information. * fortras - tools for reading and writing Fortras messages. Fortras is a EDI standard for logistics related information somewhat common in Germany. See Wikipedia_ for further enlightenment .. _Wikipedia: http://de.wikipedia.org/wiki/Fortras .. _Blogpost: https://cybernetics.hudora.biz/intern/wordpress/2010/09/dpd-routeninformationen-aktualisieren/ It also comes with the only python based `3D Bin Packing `_ implementation I'm aware of. The Algorithm has sufficient performance to be used in everyday shipping and warehousing applications. You can get the whole Package at http://pypi.python.org/pypi/pyShipping This code is BSD Licensed. .. image:: https://d2weczhvl823v0.cloudfront.net/hudora/pyshipping/trend.png :alt: Bitdeli badge :target: https://bitdeli.com/free ================================================ FILE: pyshipping/3dbpp.c ================================================ /* ====================================================================== 3D BIN PACKING, Silvano Martello, David Pisinger, Daniele Vigo 1998, 2003, 2006 ====================================================================== */ /* This code solves the three-dimensional bin-packing problem, which * asks for an orthogonal packing of a given set of rectangular-shaped * boxes into the minimum number of three-dimensional rectangular bins. * Each box j=1,..,n is characterized by a width w_j, height h_j, and * depth d_j. An unlimited number of indentical three-dimensional bins, * having width W, height H and depth D is available. The boxes have fixed * orientation, i.e., they may not be rotated. * * A description of this code is found in the following papers: * * S. Martello, D. Pisinger, D. Vigo, E. den Boef, J. Korst (2003) * "Algorithms for General and Robot-packable Variants of the * Three-Dimensional Bin Packing Problem" * submitted TOMS. * * S.Martello, D.Pisinger, D.Vigo (2000) * "The three-dimensional bin packing problem" * Operations Research, 48, 256-267 * * The present code is written in ANSI-C, and has been compiled with * the GNU-C compiler using option "-ansi -pedantic" as well as the * HP-UX C compiler using option "-Aa" (ansi standard). * * This file contains the callable routine binpack3d with prototype * * void binpack3d(int n, int W, int H, int D, * int *w, int *h, int *d, * int *x, int *y, int *z, int *bno, * int *lb, int *ub, * int nodelimit, int iterlimit, int timelimit, * int *nodeused, int *iterused, int *timeused); * * the meaning of the parameters is the following: * n Size of problem, i.e., number of boxes to be packed. * This value must be smaller than MAXBOXES defined below. * W,H,D Width, height and depth of every bin. * w,h,d Integer arrays of length n, where w[j], h[j], d[j] * are the dimensions of box j for j=0,..,n-1. * x,y,z,bno Integer arrays of length n where the solution found * is returned. For each box j=0,..,n-1, the bin number * it is packed into is given by bno[j], and x[j], y[j], z[j] * are the coordinates of it lower-left-backward corner. * lb Lower bound on the solution value (returned by the procedure). * ub Objective value of the solution found, i.e., number of bins * used to pack the n boxes. (returned by the procedure). * nodelimit maximum number of decision nodes to be explored in the * main branching tree. If set to zero, the algorithm will * run until an optimal solution is found (or timelimit or * iterlimit is reached). Measured in thousands (see IUNIT). * iterlimit maximum number of iterations in the ONEBIN algorithm * which packs a single bin. If set to zero, the algorithm will * run until an optimal solution is found (or timelimit or * nodelimit is reached). Measured in thousands (see IUNIT). * timelimit Time limit for solving the problem expressed in seconds. * If set to zero, the algorithm will run until an optimal * solution is found; otherwise it terminates after timelimit * seconds with a heuristic solution. * nodeused returns the number of branch-and-bound nodes investigated, * measured in thousands (see IUNIT). * iterused returns the number of iterations in ONEBIN algorithm, * measured in thousands (see IUNIT). * timeused returns the time used in miliseconds * * (c) Copyright 1998, 2003, 2005 * * David Pisinger Silvano Martello, Daniele Vigo * DIKU, University of Copenhagen DEIS, University of Bologna * Universitetsparken 1 Viale Risorgimento 2 * Copenhagen, Denmark Bologna, Italy * * This code can be used free of charge for research and academic purposes * only. */ #define IUNIT 1000 /* scaling factor of nodes and iterat */ #define MAXBOXES 101 /* max number of boxes (plus one box) */ #define MAXBPP 1000000 /* max numer of iterations in 1-dim bpp */ #define MAXITER 1000 /* max iterations in heuristic onebin_robot */ #define MAXCLOSE 16 /* max level for which try_close is applied */ #include #include #include #include #include #include #include /* ====================================================================== macros ====================================================================== */ #define TRUE 1 /* logical variables */ #define FALSE 0 #define WDIM 0 /* rotations of boxes */ #define HDIM 1 #define DDIM 2 #define LEFT 0 /* relative placements */ #define RIGHT 1 #define UNDER 2 #define ABOVE 3 #define FRONT 4 #define BEHIND 5 #define UNDEF 6 #define RELMAX 8 #define STACKDEPTH (MAXBOXES*MAXBOXES*RELMAX) #define VOL(i) ((i)->w * (ptype) (i)->h * (i)->d) #define MINIMUM(i,j) ((i) < (j) ? (i) : (j)) #define MAXIMUM(i,j) ((i) > (j) ? (i) : (j)) #define DIF(i,j) ((int) ((j) - (i) + 1)) #define SWAPINT(a,b) { register int t; t=*(a);*(a)=*(b);*(b)=t; } #define SWAP(a,b) { register box t; t=*(a);*(a)=*(b);*(b)=t; } #define SWAPI(a,b) { register itype t; t=(a);(a)=(b);(b)=t; } #define SWAPP(a,b) { register point t; t=*(a);*(a)=*(b);*(b)=t; } #define DF(a,b) ((r=(a).y-(b).y) != 0 ? r : (a).x-(b).x) /* ====================================================================== type declarations ====================================================================== */ typedef short boolean; /* logical variable */ typedef short ntype; /* number of states,bins */ typedef short itype; /* can hold up to W,H,D */ typedef long stype; /* can hold up to W*H*D */ typedef long ptype; /* product multiplication */ /* box record */ typedef struct irec { ntype no; /* box number */ itype w; /* box width (x-size) */ itype h; /* box height (y-size) */ itype d; /* box depth (z-size) */ itype x; /* optimal x-position */ itype y; /* optimal y-position */ itype z; /* optimal z-position */ ntype bno; /* bin number */ boolean k; /* is the box chosen? */ stype vol; /* volume of box */ struct irec *ref; /* reference to original box (if necessary) */ } box; /* all problem information */ typedef struct { itype W; /* x-size of bin */ itype H; /* y-size of bin */ itype D; /* z-size of bin */ stype BVOL; /* volume of a bin */ ntype n; /* number of boxes */ box *fbox; /* first box in problem */ box *lbox; /* last box in problem */ box *fsol; /* first box in current solution */ box *lsol; /* last box in current solution */ box *fopt; /* first box in optimal solution */ box *lopt; /* last box in optimal solution */ boolean *closed; /* for each bin indicator whether closed */ box *fclosed; /* first box in closed bins */ box *lclosed; /* last box in closed bins */ ntype noc; /* number of closed bins */ itype mindim; /* currently smallest box length */ itype maxdim; /* currently largest box length */ stype maxfill; /* the best filling found */ int mcut; /* how many siblings at each node in b&b */ /* different bounds */ ntype bound0; /* Bound L_0 at root node */ ntype bound1; /* Bound L_1 at root node */ ntype bound2; /* Bound L_2 at root node */ ntype lb; /* best of the above */ ntype z; /* currently best solution */ /* controle of 3d filler */ int maxiter; /* max iterations in onebin_robot */ int miss; /* number boxes not packed in onebin_robot */ /* debugging and controle information */ int nodes; /* nodes in branch-and-bound */ int iterat; /* iterations in onebin_decision */ int subnodes; /* nodes in branch-and-bound */ int subiterat; /* iterations in onebin_decision */ int exfill; /* number of calls to onebin_decision */ int iter3d; /* iterations in onebin_robot or general */ int zlayer; /* heuristic solution layer */ int zmcut; /* heuristic solution mcut */ double exacttopo; /* number of topological sorts */ double exacttopn; /* number of topological sorts */ int exactcall; /* number of calls to exact */ int exactn; /* largest problem for exact */ double genertime; /* time used in onebin_general */ double robottime; /* time used in onebin_robot */ double time; /* computing time */ double lhtime; /* layer heuristic computing time */ double mhtime; /* mcut heuristic computing time */ int didpush; /* did the lower bound push up bound */ int maxclose; /* max number of closed bins at any time */ int nodelimit; /* maximum number of nodes in main tree */ int iterlimit; /* maximum number of iterations in ONEBIN*/ int timelimit; /* maximum amount of time to be used */ } allinfo; /* structure for greedy algorithm */ typedef struct { int lno; /* layer number */ int d; /* depth of layer */ int bno; /* bin no assigned to layer */ int z; /* z level of layer within bin */ int b; /* temporary bin number */ } heurpair; /* structure for extreme points in a single bin */ typedef struct { itype x; /* x-coordinate */ itype y; /* y-coordinate */ itype z; /* z-coordinate */ } point; /* structure for a domain pair in constraint programming */ typedef struct { int i; /* index of box i */ int j; /* index of box j */ int relation; /* relation between the two boxes */ boolean domain; /* domain of the two boxes */ } domainpair; /* set of domains */ typedef char domset[RELMAX]; typedef domset domline[MAXBOXES]; /* pointer to comparison function */ typedef int (*funcptr) (const void *, const void *); /* ====================================================================== global variables ====================================================================== */ /* boolean variable to indicate time-out situation */ boolean stopped; /* counter used to ensure that 1D BPP at most performs MAXBPP iterations */ int bpiterat; /* boolean variables to indicate when 1D packing algorithm should terminate */ boolean feasible, terminate; /* stack of domain pairs */ domainpair domstack[STACKDEPTH]; domainpair *dompos, *domend; /* domain of each box */ domline domain[MAXBOXES]; /* current relation between two boxes */ char relation[MAXBOXES][MAXBOXES]; /* debug variable to see level in recursive packing algorithm */ int bblevel; /* ======================================================================= error ======================================================================= */ void error(char *str, ...) { va_list args; va_start(args, str); vprintf(str, args); printf("\n"); va_end(args); printf("IRREGULAR PROGRAM TERMINATION\n"); exit(-1); } /* ********************************************************************** ********************************************************************** Timing routines ********************************************************************** ********************************************************************** */ /* This timing routine is based on the ANSI-C procedure "clock", which * has a resolution of 1000000 ticks per second. This however implies * that we pass the limit of a long integer after only 4295 seconds. * The following routine attempts to correct such situations by adding * the constant ULONG_MAX to the counter whenever wraparound can be * detected. But the user is advised to use a timing routine like "times" * (which however is not ANSI-C standard) for measuring longer time * periods. */ void timer(double *time) { static double tstart, tend, tprev; if (time == NULL) { clock(); /* one extra call to initialize clock */ tstart = tprev = clock(); } else { tend = clock(); if (tend < tprev) tstart -= ULONG_MAX; /* wraparound occured */ tprev = tend; *time = (tend-tstart) / CLOCKS_PER_SEC; /* convert to seconds */ } } /* test for time limit */ void check_timelimit(long max) { double t; if (max == 0) return; timer(&t); if (t >= max) { if (!stopped) printf("TIMELIMIT\n"); stopped = TRUE; } } /* test for node limit */ void check_nodelimit(long nodes, long max) { if (max == 0) return; if (nodes >= max) { if (!stopped) printf("NODELIMIT\n"); stopped = TRUE; } } /* test for iteration limit */ void check_iterlimit(long iterations, long max) { if (max == 0) return; if (iterations >= max) { if (!stopped) printf("ITERLIMIT\n"); stopped = TRUE; } } /* ********************************************************************** ********************************************************************** Small procedures ********************************************************************** ********************************************************************** */ /* ====================================================================== simple comparisions ====================================================================== */ /* Comparisons used as argument to qsort. */ int dcomp(box *a, box *b) { int r; r = b->d - a->d; if (r != 0) return r; else return b->no - a->no; } int hcomp(box *a, box *b) { int r; r = b->h - a->h; if (r != 0) return r; else return b->no - a->no; } int vcomp(box *a, box *b) /* volume decr. */ { int r; r = b->vol-a->vol; if (r != 0) return r; else return b->no - a->no; } int xcomp(heurpair *a, heurpair *b) /* depth decr. */ { int r; r = b->d - a->d; if (r != 0) return r; else return b->lno - a->lno; } int lcomp(heurpair *a, heurpair *b) /* layer number decr. */ { int r; r = a->lno-b->lno; if (r != 0) return r; else return b->d - a->d; } /* ====================================================================== palloc ====================================================================== */ /* Memory allocation and freeing, with implicit check */ void *palloc(long sz, long no) { long size; void *p; size = sz * no; if (size == 0) size = 1; p = (void *) malloc(size); if (p == NULL) error("no memory size %ld", size); return p; } void pfree(void *p) { if (p == NULL) error("freeing null"); free(p); } /* ====================================================================== checksol ====================================================================== */ /* Check correctnes of solution, i.e., no boxes overlap, no duplicated boxes. */ void checksol(allinfo *a, box *f, box *l) { box *i, *j, *m; for (i = f, m = l+1; i != m; i++) { if (!i->k) continue; /* box currently not chosen */ for (j = f; j != m; j++) { if (i == j) continue; if (i->no == j->no) error("duplicated box %d\n", i->no); if (!j->k) continue; if (i->bno != j->bno) continue; if ((i->x + i->w > j->x) && (j->x + j->w > i->x) && (i->y + i->h > j->y) && (j->y + j->h > i->y) && (i->z + i->d > j->z) && (j->z + j->d > i->z)) { error("overlap box %d,%d: [%d,%d,%d] [%d,%d,%d]", i->no, j->no, i->w, i->h, i->d, j->w, j->h, j->d); } } } } /* ====================================================================== savesol ====================================================================== */ /* save an updated solution, checking its validity */ void savesol(allinfo *a, box *f, box *l, ntype z) { box *i, *k, *m; /* first check validity */ if (z >= a->z) error("not improved"); for (i = f, m = l+1; i != m; i++) { if ((1 <= i->bno) && (i->bno <= z)) continue; error("illegal bin %d, box %d", i->bno, i->no); } /* now do the saving */ a->z = z; for (i = f, k = a->fopt, m = l+1; i != m; i++, k++) *k = *i; for (i = a->fclosed, m = a->lclosed+1; i != m; i++, k++) *k = *i; for (i = a->fopt, m = a->lopt+1; i != m; i++) i->k = TRUE; if (DIF(a->fopt,k-1) != a->n) error("not correct amount of boxes"); checksol(a, a->fopt, a->lopt); } /* ====================================================================== isortincr ====================================================================== */ /* A specialized routine for sorting integers in increasing order. */ /* qsort could be used equally well, but this routine is faster. */ void isortincr(int *f, int *l) { register int mi; register int *i, *j, *m; register int d; d = l - f + 1; if (d < 1) error("negative interval in isortincr"); if (d == 1) return; m = f + d / 2; if (*f > *m) SWAPINT(f, m); if (d > 2) { if (*m > *l) { SWAPINT(m, l); if (*f > *m) SWAPINT(f, m); } } if (d <= 3) return; mi = *m; i = f; j = l; for (;;) { do i++; while (*i < mi); do j--; while (*j > mi); if (i > j) break; else SWAPINT(i, j); } isortincr(f, i-1); isortincr(i, l); } /* ====================================================================== psortdecr ====================================================================== */ /* A specialized routine for sorting extreme points according to decreasing */ /* y-coordinate (decreasing x-coordinate in case of ties) */ void psortdecr(point *f, point *l) { register point mi; register point *i, *j, *m; register int d, r; d = l - f + 1; if (d <= 1) return; m = f + d / 2; if (DF(*f,*m)<0) SWAPP(f,m); if (d == 2) return; if (DF(*m,*l)<0) { SWAPP(m,l); if (DF(*f,*m)<0) SWAPP(f,m); } if (d <= 3) return; mi = *m; i = f; j = l; for (;;) { do i++; while (DF(*i,mi) > 0); do j--; while (DF(*j,mi) < 0); if (i > j) break; else SWAPP(i, j); } psortdecr(f, i-1); psortdecr(i, l); } /* ********************************************************************** ********************************************************************** Lower Bounds ********************************************************************** ********************************************************************** */ /* ====================================================================== bound_zero ====================================================================== */ /* The continuous bound L_0 */ int bound_zero(allinfo *a, box *f, box *l) { box *i, *m; stype vsum, lb; vsum = 0; for (i = f, m = l+1; i != m; i++) vsum += i->vol; lb = (stype) ceil(vsum / (double) a->BVOL); return lb; } /* ====================================================================== rotate_solution ====================================================================== */ /* rotates the solution. After 3 rotations we return to original problem */ void rotate_solution(allinfo *a, box *f, box *l) { register box *i, *m; register itype w, x; for (i = f, m = l+1; i != m; i++) { w = i->w; i->w = i->h; i->h = i->d; i->d = w; x = i->x; i->x = i->y; i->y = i->z; i->z = x; } } /* ====================================================================== rotate_problem ====================================================================== */ /* rotates the dimensions by one step */ void rotate_problem(allinfo *a, box *f, box *l) { register box *i, *m; register itype w, x; for (i = f, m = l+1; i != m; i++) { w = i->w; i->w = i->h; i->h = i->d; i->d = w; x = i->x; i->x = i->y; i->y = i->z; i->z = x; } w = a->W; a->W = a->H; a->H = a->D; a->D = w; } /* ====================================================================== choose_boxes ====================================================================== */ /* returns a set of boxes with w > W2 and d > D2. This set is used in */ /* bound_one */ void choose_boxes(allinfo *a, box *f, box *l, int W2, int D2, box *fbox , box **lbox) { box *i, *k, *m; for (i = f, m = l+1, k = fbox; i != m; i++) { if ((i->w > W2) && (i->d > D2)) { *k = *i; k++; } } *lbox = k-1; } /* ====================================================================== find_plist ====================================================================== */ /* returns a zero-terimanted list of distinct dimensions */ void find_plist(box *fbox, box *lbox, itype M, int dim, int *pl) { register box *i, *m; register int *k, *j, *l; i = fbox; m = lbox+1; k = pl; switch (dim) { case WDIM: for (; i != m; i++) { if (i->w <= M) { *k = i->w; k++; } } break; case HDIM: for (; i != m; i++) { if (i->h <= M) { *k = i->h; k++; } } break; case DDIM: for (; i != m; i++) { if (i->d <= M) { *k = i->d; k++; } } break; } if (k == pl) { *k = 0; return; } isortincr(pl, k-1); /* sort the dimensions */ for (j = pl+1, l = pl; j != k; j++) { /* remove duplicates */ if (*j != *l) { l++; *l = *j; } } l++; *l = 0; } /* ====================================================================== bound_one ====================================================================== */ /* Derive bound L_1 for a fixed dimension */ int bound_one_x(allinfo *a, box *f, box *l) { register box *i, *m; register itype H, H2, h; register int p, j1, j2, j3, j2h, j2hp, j3h; int *pp, lb, lb_one, alpha, beta; box fbox[MAXBOXES], *lbox; int plist[MAXBOXES]; if (l == f-1) return 0; lb = 1; H = a->H; H2 = H/2; choose_boxes(a, f, l, a->W/2, a->D/2, fbox, &lbox); if (lbox == fbox-1) { /* empty */ return lb; } find_plist(fbox, lbox, H2, HDIM, plist); for (pp = plist; *pp != 0; pp++) { p = *pp; j1 = j2 = j3 = j2h = j2hp = j3h = 0; for (i = fbox, m = lbox+1; i != m; i++) { h = i->h; if (h > H-p) j1++; if ((H-p >= h) && (h > H2)) { j2++; j2h += h; j2hp += (H-h)/p; } if ((H2 >= h) && (h >= p)) { j3++; j3h += h; } } alpha = (int) ceil((j3h - (j2 * H - j2h)) / (double) H); beta = (int) ceil((j3 - j2hp) / (double) (H/p)); if (alpha < 0) alpha = 0; if (beta < 0) beta = 0; lb_one = j1 + j2 + MAXIMUM(alpha, beta); if (lb_one > lb) lb = lb_one; } return lb; } /* Derive bound L_1 as the best of all L_1 bounds for three rotations */ int bound_one(allinfo *a, box *f, box *l) { int i, lb, lbx; lb = 0; for (i = WDIM; i <= DDIM; i++) { lbx = bound_one_x(a, f, l); if (lbx > lb) lb = lbx; rotate_problem(a, f, l); } return lb; } /* ====================================================================== bound_two ====================================================================== */ /* Derive bound L_2 for a fixed dimension */ int bound_two_x(allinfo *a, box *f, box *l) { register box *i, *m; register itype W, H, D, w, h, d, W2, D2; register int p, q, k1h, k23v; int hlb1, lb, lb1, lbx, fract; int plist[MAXBOXES], qlist[MAXBOXES]; int *qq, *pp; double WD, BVOL; /* derive bound_one */ lb = lb1 = bound_one_x(a, f, l); W = a->W; H = a->H; D = a->D; hlb1 = H * lb1; W2 = W/2; D2 = D/2; WD = W*(double)D; BVOL = a->BVOL; /* run through all values of p, q */ find_plist(f, l, W2, WDIM, plist); find_plist(f, l, D2, DDIM, qlist); for (pp = plist; *pp != 0; pp++) { p = *pp; for (qq = qlist; *qq != 0; qq++) { q = *qq; k1h = k23v = 0; for (i = f, m = l+1; i != m; i++) { w = i->w; h = i->h; d = i->d; if ((w > W - p) && (d > D - q)) { k1h += h; continue; } if ((w >= p) && (d >= q)) { k23v += i->vol; } } fract = (int) ceil((k23v - (hlb1 - k1h)*WD) / BVOL); if (fract < 0) fract = 0; lbx = lb1 + fract; if (lbx > lb) lb = lbx; } } return lb; } /* Derive bound L_2 as the best of all L_2 bounds for three rotations */ int bound_two(allinfo *a, box *f, box *l) { int i, lb, lbx; lb = 0; for (i = WDIM; i <= DDIM; i++) { lbx = bound_two_x(a, f, l); if (lbx > lb) lb = lbx; rotate_problem(a, f, l); } return lb; } /* ********************************************************************** ********************************************************************** heuristic filling ********************************************************************** ********************************************************************** */ /* ====================================================================== onelayer ====================================================================== */ /* Fill a layer of depth f->d by arranging the boxes in a number of */ /* vertical shelfs. The boxes $i$ packed are assigned coordinates */ /* (i->x, i->y) and the field i->k is set to the argument d (layer no). */ void onelayer(allinfo *a, box *f, box *m, box *l, int d) { int s, t; itype r; /* remaining width */ itype width[MAXBOXES], height[MAXBOXES], x[MAXBOXES]; box *i; qsort(f, DIF(f,m), sizeof(box), (funcptr) hcomp); r = a->W; x[0] = 0; width[0] = 0; height[0] = 0; for (s = 1, i = f; i != l+1; s++) { x[s] = x[s-1] + width[s-1]; height[s] = 0; width[s] = i->w; if (width[s] > r) width[s] = r; r -= width[s]; for ( ; i != l+1; i++) { for (t = s; t != 0; t--) { if (i->w <= width[t]) { if (height[t] + i->h <= a->H) { i->y = height[t]; i->x = x[t]; i->k = d; height[t] += i->h; break; } } } if ((t == 0) && (r > 0)) break; /* new strip */ } } } /* ====================================================================== countarea ====================================================================== */ /* Select a subset of the boxes such that the selected boxes have a total */ /* area of two times the face of a bin (the parameter: barea) */ box *countarea(box *f, box *l, stype barea) { box *i; stype area, d; for (area = 0, i = f; i != l+1; i++) { d = i->h * (ptype) i->w; area += d; if (area > 2*barea) return i-1; } return l; } /* ====================================================================== remboxes ====================================================================== */ /* Remove the boxes which were chosen for a layer, i.e., where i->k != 0. */ /* The depth of the layer is set equal to the deepest box chosen. */ box *remboxes(box *f, box *l, itype *depth) { box *i, *j; itype d; for (i = f, j = l, d = 0; i != j+1; ) { if (i->k) { if (i->d > d) d = i->d; i++; } else { SWAP(i,j); j--; } } *depth = d; return i; } /* ====================================================================== assignboxes ====================================================================== */ /* Assign z-coordinates to the boxes, once they layers have been combined */ /* to individual bins by solving a 1-dimensional Bin-packing Problem. */ void assignboxes(heurpair *t, heurpair *u, ntype maxbin, box *f, box *l) { box *i, *m; heurpair *h; itype b, z; /* derive z-coordinates for each layer */ for (b = 1; b <= maxbin; b++) { z = 0; for (h = t; h <= u; h++) if (h->bno == b) { h->z = z; z += h->d; } } for (i = f, m = l+1; i != m; i++) { h = t + i->k - 1; i->z = h->z; i->bno = h->bno; } } /* ====================================================================== onedim_binpack ====================================================================== */ /* One-dimensional bin-packing algorithm. In each iteration, the next */ /* box is assigned to every open bin as well as to a new bin. The */ /* algorithm terminates when MAXBPP iterations have been performed, */ /* returning the heuristic solution found. */ void onedim_binpack(heurpair *i, heurpair *f, heurpair *l, int *b, int bno, itype *z) { int j, *bc; heurpair *k; bpiterat++; if (bpiterat > MAXBPP) return; if (bno >= *z) return; /* no hope of improvement */ if (i > l) { *z = bno; for (k = f; k <= l; k++) k->bno = k->b; } else { for (j = 0; j < bno; j++) { bc = b + j; if (i->d <= *bc) { *bc -= i->d; i->b = j+1; onedim_binpack(i+1, f, l, b, bno, z); *bc += i->d; } } bc = b + bno; *bc -= i->d; i->b = bno+1; onedim_binpack(i+1, f, l, b, bno+1, z); *bc += i->d; } } /* ====================================================================== dfirst_heuristic ====================================================================== */ /* Heuristic algorithm for the 3D BPP. A number of layers are constructed */ /* using the shelf approach to pack every layer. Then the individual layers */ /* are combined to bins by solving a 1D BPP defined in the layer depths. */ void dfirst_heuristic(allinfo *a) { box *j, *f, *l, *m; int i, n, h, b[MAXBOXES]; heurpair t[MAXBOXES]; itype d, z; /* initialize boxes */ for (j = a->fbox, m = a->lbox+1; j != m; j++) { j->bno = j->x = j->y = j->z = 0; j->k = FALSE; } /* fill layer one by one */ for (f = a->fbox, l = a->lbox, h = 0; ; h++) { n = DIF(f,l); if (n == 0) break; qsort(f, n, sizeof(box), (funcptr) dcomp); m = countarea(f, l, a->W * (ptype) a->H); onelayer(a, f, m, l, h+1); f = remboxes(f, l, &d); t[h].d = d; t[h].bno = h+1; /* initially put layers into separate bins */ t[h].z = 0; t[h].lno = h+1; /* this ensures fes. solution if terminate */ } /* split into bins by solving 1-dim binpacking */ for (i = 0; i < h; i++) b[i] = a->D; /* all bins are empty */ qsort(t, h, sizeof(heurpair), (funcptr) xcomp); z = h+1; bpiterat = 0; onedim_binpack(t, t, t+h-1, b, 0, &z); qsort(t, h, sizeof(heurpair), (funcptr) lcomp); /* order according to lno */ /* now assign bin number to each boxes */ assignboxes(t, t+h-1, z, a->fbox, a->lbox); if (z < a->zlayer) a->zlayer = z; if (a->zlayer < a->z) savesol(a, a->fbox, a->lbox, a->zlayer); } /* ====================================================================== dfirst3_heuristic ====================================================================== */ /* Call the heuristic dfirst_heuristic, for three different rotations */ /* of the problem */ void dfirst3_heuristic(allinfo *a) { int i; double t1, t2; timer(&t1); a->zlayer = a->n; /* very bad incumbent solution */ for (i = WDIM; i <= DDIM; i++) { dfirst_heuristic(a); rotate_solution(a, a->fopt, a->lopt); rotate_problem(a, a->fbox, a->lbox); } timer(&t2); a->lhtime = t2 - t1; } /* ********************************************************************** ********************************************************************** fill one 3D bin using GENERAL packing ********************************************************************** ********************************************************************** */ /* ====================================================================== modifyandpush ====================================================================== */ /* Push the relation "rel" between box i and box j to a stack. If "dom" * is true, the relation is removed from the domain. If "dom" is false, * the relation "rel" is imposed between boxes "i" and "j". */ void modifyandpush(int i, int j, int rel, boolean dom) { dompos->i = i; dompos->j = j; dompos->domain = dom; if (dom) { dompos->relation = rel; domain[i][j][rel] = FALSE; } else { dompos->relation = relation[i][j]; relation[i][j] = rel; } dompos++; if (dompos == domend) error("stack filled\n"); } /* ====================================================================== popdomains ====================================================================== */ /* Pop all relations between boxes from the stack. The stack is emptied * downto the depth given by "pos". */ void popdomains(domainpair *pos) { for (; dompos != pos; ) { dompos--; if (dompos->domain) { domain[dompos->i][dompos->j][dompos->relation] = TRUE; } else { relation[dompos->i][dompos->j] = dompos->relation; } } } /* ====================================================================== findcoordinates ====================================================================== */ /* Find coordinates of boxes according to the currently pending relations. * In principle this can be done by topologically sorting the boxes according * to e.g. the left-right relations, and then assigning coordinates from * the left-most box to the right-most box. * The following implementation is a simplified version, which runs through * all pairs of boxes, and checks whether they satisfy the relation, otherwise * moving one of the boxes right, up or behind, according to the given * relation. The process is repeated until no pairs of boxes violate a * relation. Computational experiments have shown that the present approach * for the considered instances is faster than a topological sorting followed * by a critical path calculation. * If a box during the process gets moved outsides of the bin, then * the algorithm terminates with FALSE. Otherwise TRUE is returned, saying * that a feasible packing exists which respects the current relations. */ boolean findcoordinates(allinfo *a, int n, box *f) { register box *g, *h; register int sum; int i, j, k, W, H, D; boolean changed; char *dom, *relij; domset *domij; /* check if feasible, i.e., at least one choice for each relation */ W = a->W; H = a->H; D = a->D; for (i = 0; i < n; i++) { j = i+1; relij = &(relation[i][j]); domij = &(domain[i][j]); for ( ; j < n; j++, relij++, domij++) { if (*relij != UNDEF) continue; dom = *domij; if (*dom) continue; dom++; if (*dom) continue; dom++; if (*dom) continue; dom++; if (*dom) continue; dom++; if (*dom) continue; dom++; if (*dom) continue; return FALSE; } } /* initialize coordinates */ for (i = 0; i < n; i++) { g = f+i; g->x = g->y = g->z = 0; } /* now determine the coordinates */ a->exacttopo++; for (k = 0; k < n; k++) { a->exacttopn++; changed = FALSE; for (i = 0; i < n; i++) { g = f+i; j = i+1; relij = &(relation[i][j]); for ( ; j < n; j++, relij++) { h = f+j; switch (*relij) { case UNDEF : /* do nothing */ break; case LEFT : sum = g->x + g->w; if (h->x < sum) { h->x = sum; changed = TRUE; if (sum + h->w > W) return FALSE; } break; case RIGHT : sum = h->x + h->w; if (g->x < sum) { g->x = sum; changed = TRUE; if (sum + g->w > W) return FALSE; } break; case UNDER : sum = g->y + g->h; if (h->y < sum) { h->y = sum; changed = TRUE; if (sum + h->h > H) return FALSE; } break; case ABOVE : sum = h->y + h->h; if (g->y < sum) { g->y = sum; changed = TRUE; if (sum + g->h > H) return FALSE; } break; case FRONT : sum = g->z + g->d; if (h->z < sum) { h->z = sum; changed = TRUE; if (sum + h->d > D) return FALSE; } break; case BEHIND: sum = h->z + h->d; if (g->z < sum) { g->z = sum; changed = TRUE; if (sum + g->d > D) return FALSE; } break; } } } if (!changed) { return TRUE; } } /* there must be a loop in the graph */ return FALSE; } /* ====================================================================== checkdomain ====================================================================== */ /* Temporarily impose the relation "value" between boxes "i" and "j", * and check whether a feasible assignment of coordinates exists which * respects all currently imposed relations. * If the relation cannot be satisfied, it is removed from the domain * and pushed to a stack, so that it can be restored upon backtracking. */ void checkdomain(allinfo *a, int i, int j, int n, box *f, int value) { if (domain[i][j][value] == FALSE) return; /* not allowed in any case */ relation[i][j] = value; if (findcoordinates(a, n, f) == FALSE) { modifyandpush(i, j, value, TRUE); } } /* ====================================================================== reducedomain ====================================================================== */ /* Constraint propagation algorithm. For each relation in the domain of * boxes "i" and "j", check if the relation has the posibility of being * satisfied. If some of the relations cannot be satisfied any more, they * are removed from the domain (and pushed to a stack, so that they can * be restored when the master search algorithm backtracks). If only one * relation remains in the domain, the relation is imposed at this node * and all descendant nodes. */ boolean reducedomain(allinfo *a, int n, box *f) { register int i, j, k, l, m; m = 0; for (i = 0; i < n-1; i++) { for (j = i+1; j < n-1; j++) { if (relation[i][j] == UNDEF) { checkdomain(a, i, j, n, f, LEFT); checkdomain(a, i, j, n, f, RIGHT); checkdomain(a, i, j, n, f, UNDER); checkdomain(a, i, j, n, f, ABOVE); checkdomain(a, i, j, n, f, FRONT); checkdomain(a, i, j, n, f, BEHIND); relation[i][j] = UNDEF; for (k = LEFT, l = 0; k < UNDEF; k++) { if (domain[i][j][k]) { l++; m = k; } } if (l == 0) return FALSE; if (l == 1) { modifyandpush(i, j, m, FALSE); } } } } return TRUE; } /* ====================================================================== recpack ====================================================================== */ /* Recursive algorithm based on constraint programming used for assigning * relative positions to each pair of boxes. Each pair of boxes initially * has an associated relation with domain LEFT, RIGHT, UNDER, ABOVE, FRONT, * BEHIND. In each iteration of the algorithm a pair of boxes "i" and "j" * is assigned the relation "rel". Constraint propagation is then used to * decrease the domains of remaining relations. * If it is not possible to assign coordinates to the boxes such that the * currently imposed relations between pairs of boxes are respected, we * backtrack. * If each pair of boxes has been assigned a relation, and it is possible * to assign coordinates to the boxes such that the currently imposed * relations between pairs of boxes are respected, we save the solution * and return. * Otherwise, constraint propagation is used to decrease the domains * of relations corresponding to each pairs of boxes. If a domain only * contains a single relation, the relation is fixed. * The recursive step selects the next pair of boxes following "i" and "j" * and repeatedly assigns each relation from the domain to the relation * variable. */ void recpack(allinfo *a, int i, int j, int n, box *f, int rel) { int i1, j1; domainpair *pos; boolean feas; if (stopped) return; a->iter3d++; if ((a->iter3d == a->maxiter) && (a->maxiter != 0)) terminate = TRUE; a->subiterat++; if (a->subiterat == IUNIT) { a->subiterat = 0; a->iterat++; check_iterlimit(a->iterat, a->iterlimit); check_timelimit(a->timelimit); } if (terminate) return; relation[i][j] = rel; for (i1 = 0, j1 = 0; i1 != i && j1 != j; ) { i1++; if (i1 >= j1) { i1 = 0; j1++; } if (relation[i1][j1] == UNDEF) error("relation error %d %d\n", i1, j1); } feas = findcoordinates(a, n, f); if (!feas) return; if ((i == n-2) && (j == n-1)) { feasible = TRUE; terminate = TRUE; memcpy(a->fsol, f, sizeof(box) * n); return; } pos = dompos; feas = reducedomain(a, n, f); if (feas) { i++; if (i >= j) { i = 0; j++; } bblevel++; rel = relation[i][j]; if (domain[i][j][LEFT ]) recpack(a, i, j, n, f, LEFT); if (domain[i][j][RIGHT]) recpack(a, i, j, n, f, RIGHT); if (domain[i][j][UNDER]) recpack(a, i, j, n, f, UNDER); if (domain[i][j][ABOVE]) recpack(a, i, j, n, f, ABOVE); if (domain[i][j][FRONT]) recpack(a, i, j, n, f, FRONT); if (domain[i][j][BEHIND])recpack(a, i, j, n, f, BEHIND); relation[i][j] = rel; bblevel--; } popdomains(pos); } /* ====================================================================== general_pack ====================================================================== */ /* General packing procedure, which tests whether boxes f..l can be packed * into a single bin. The algorithm is based on constraint programming, where * each pair of boxes initially has an associated relation with domain LEFT, * RIGHT, UNDER, ABOVE, FRONT, BEHIND. Then the recursive algorithm "recpack" * is called, which repeatedly tries to assign the relation a value, using * constraint propagation to decrease the domains of remaining boxes. */ boolean general_pack(allinfo *a, box *f, box *l) { register int i, j, k, n; dompos = domstack; domend = domstack + STACKDEPTH; feasible = FALSE; terminate = FALSE; bblevel = 1; n = l-f+1; if (n > a->exactn) a->exactn = n; for (i = 0; i < n; i++) { for (j = 0; j < n; j++) { relation[i][j] = UNDEF; for (k = LEFT; k < UNDEF; k++) { domain[i][j][k] = TRUE; } } } domain[0][1][RIGHT ] = FALSE; domain[0][1][ABOVE ] = FALSE; domain[0][1][BEHIND] = FALSE; recpack(a, 0, 0, n, f, UNDEF); return feasible; } /* ====================================================================== onebin_general ====================================================================== */ /* Check if boxes f..l can be packed into a single bin using general * packing. If "fast" is TRUE, the problem is only solved heuristically, * hence if the algorithm returns FALSE we cannot rule out the posibility * of packing all boxes into the bin. */ boolean onebin_general(allinfo *a, box *f, box *l, boolean fast) { boolean solution; double t1, t2; /* check time limit */ if (stopped) return FALSE; a->iter3d = 0; a->maxiter = (fast ? MAXITER : 0); /* limited or infinitly many */ a->exactcall++; /* calling general pack */ timer(&t1); solution = general_pack(a, f, l); if (solution) checksol(a, f, l); timer(&t2); a->genertime += t2 - t1; return solution; } /* ====================================================================== envelope ====================================================================== */ /* Find the two-dimensional envelope of the boxes given by extreme */ /* points f to l. */ void envelope(point *f, point *l, point *s1, point **sm, itype W, itype H, itype D, itype RW, itype RH, itype cz, point **ll, int *nz, stype *area) { register point *i, *s, *t; register itype x, xx, y, z, ix, iy, iz, mz; register stype sum; /* find corner points and area */ x = xx = z = 0; y = H; sum = 0; mz = D; for (i = t = f, s = s1; i != l; i++) { iz = i->z; if (iz <= cz) continue; if (iz < mz) mz = iz; /* find minimum next z coordinate */ ix = i->x; if (ix <= x) { if (iz > z) { *t = *i; t++; } continue; } iy = i->y; if ((x <= RW) && (iy <= RH)) { s->x = x; s->y = iy; s->z = cz; s++; sum += (x - xx) * (ptype) y; y = iy; xx = x; } x = ix; z = iz; *t = *i; t++; } if (y != 0) sum += (W - xx) * (ptype) y; *sm = s-1; *area = sum; *nz = mz; *ll = t-1; } /* ====================================================================== checkdom ====================================================================== */ /* The 3D envelope is found by deriving a number of 2D envelopes. This */ /* may however introduce some "false" corner points, which are marked */ /* by the following algorithm. */ void checkdom(point *s1, point *sl, point *sm) { register point *s, *t, *u; if (sl == s1-1) return; for (s = s1, t = sl+1, u = sm+1; t != u; t++) { while (s->x < t->x) { s++; if (s > sl) return; } if ((s->x == t->x) && (s->y == t->y)) t->z = 0; } } /* ====================================================================== removedom ====================================================================== */ /* Remove "false" corner points marked by algorithm "checkdom". */ point *removedom(point *s1, point *sl) { register point *i, *m, *k; for (i = k = s1, m = sl+1; i != m; i++) { if (i->z == 0) continue; *k = *i; k++; } return k-1; } /* ====================================================================== initboxes ====================================================================== */ /* Initialize boxes. Already placed boxes define extreme points, and thus */ /* they are placed into the list fc,..,lc. Not placed boxes are used to */ /* derive minimum dimensions. */ void initboxes(box *f, box *l, point *fc, point **lc, int *minw, int *minh, int *mind) { register box *j, *m; register point *k; register int minx, miny, minz; minx = *minw; miny = *minh; minz = *mind; for (j = f, k = fc, m = l+1; j != m; j++) { if (j->k) { /* defines an extreme box */ k->x = j->x+j->w; k->y = j->y+j->h; k->z = j->z+j->d; k++; } else { /* free box */ if (j->w < minx) minx = j->w; if (j->h < miny) miny = j->h; if (j->d < minz) minz = j->d; } } *minw = minx; *minh = miny; *mind = minz; *lc = k-1; } /* ====================================================================== findplaces ====================================================================== */ /* Find all corner points, where a new box may be placed as well as the */ /* volume of the "envelope" occupied by already placed boxes. Already */ /* placed boxes are given by f,..,l, while a list of possible placings */ /* is returned in s1,..,sm. The return value of the procedure is an upper */ /* bound on the possible filling of this bin. */ stype findplaces(allinfo *a, box *f, box *l, point *s1, point **sm, stype fill) { register point *k; int minw, minh, mind, W, H, D, RW, RH, z, zn; point *sk, *lc, *sl, *st, *s0; stype vol, area; point fc[MAXBOXES+1]; /* select boxes which are chosen, and find min dimensions of unchosen */ minw = W = a->W; minh = H = a->H; mind = D = a->D; initboxes(f, l, fc, &lc, &minw, &minh, &mind); /* sort the boxes according to max y (first) max x (second) */ if (lc >= fc) psortdecr(fc, lc); /* order decreasing */ /* for each z-coordinate find the 2D envelope */ vol = 0; sl = s1-1; sk = s1; s0 = NULL; RW = W - minw; RH = H - minh; lc++; k = lc; k->x = W+1; k->y = 0; k->z = a->D+1; for (z = 0; z != D; z = zn) { /* find 2D envelope for all boxes which cover *z */ envelope(fc, lc+1, sl+1, &st, W, H, D, RW, RH, z, &lc, &zn, &area); if (zn + mind > D) zn = D; /* nothing fits between zn and D */ vol += area * (ptype) (zn - z); /* update volume */ checkdom(sk, sl, st); /* check for dominance */ sk = sl+1; sl = st; if (z == 0) s0 = sl; } *sm = removedom(s0+1, sl); return fill + (a->BVOL - vol); /* bound is curr filling + all free vol */ } /* ====================================================================== branch ====================================================================== */ /* Recursive algorithm for solving a knapsack filling of a single bin. */ /* In each iteration, the set of feasible positions for placing a new */ /* box is found, and an upper bound on the filling is derived. If the */ /* bound indicates that an improved solution still may be obtained, every */ /* box is tried to be placed at every feasible position, before calling */ /* the algorithm recursively. */ void branch(allinfo *a, box *f, box *l, int miss, stype fill) { register box *i; register point *s; int d; stype bound; point *sl, s1[9*MAXBOXES]; if (stopped) return; a->iter3d++; if ((a->iter3d == a->maxiter) && (a->maxiter != 0)) terminate = TRUE; if (a->iter3d % 1000 == 0) check_timelimit(a->timelimit); a->subiterat++; if (a->subiterat == IUNIT) { a->subiterat = 0; a->iterat++; check_iterlimit(a->iterat, a->iterlimit); } if (terminate) return; /* find min/max dimensions of remaining boxes */ if (miss == 0) { /* none left -> good: save solution */ memcpy(a->fsol, f, sizeof(box) * DIF(f,l)); a->maxfill = a->BVOL; terminate = TRUE; a->miss = miss; } else { /* check if better filling */ if (fill > a->maxfill) { memcpy(a->fsol, f, sizeof(box) * DIF(f,l)); a->maxfill = fill; a->miss = miss; } /* find bound and positions to place new boxes */ bound = findplaces(a, f, l, s1, &sl, fill); if (bound > a->maxfill) { /* for each position in S, try to place an box there */ for (s = s1; s != sl+1; s++) { for (i = f, d = 0; i != l+1; i++) { if (i->k) continue; /* already chosen */ /* see if box fits at position s */ if ((int) (s->x) + i->w > a->W) continue; if ((int) (s->y) + i->h > a->H) continue; if ((int) (s->z) + i->d > a->D) continue; /* place box and call recursively */ i->k = TRUE; i->x = s->x; i->y = s->y; i->z = s->z; branch(a, f, l, miss - 1, fill + i->vol); i->k = FALSE; i->x = i->y = i->z = 0; d++; if (d == a->mcut) break; /* terminate after mcut branches */ if (terminate) break; } } } } } /* ====================================================================== mcut_heuristic ====================================================================== */ /* Knapsack filling of a single bin, solved heuristically. The heuristic */ /* is based on the exact algorithm for knapsack filling a single bin, where */ /* only a limited number of sub-nodes are considered at every branching node */ /* (the so-called m-cut approach) */ void mcut_heuristic(allinfo *a) { box *f, *l, *i, *j, *m; int b, n; /* initialize boxes */ for (i = a->fbox, m = a->lbox+1; i != m; i++) { i->bno = i->x = i->y = i->z = 0; i->k = FALSE; } /* fill bins one by one */ f = a->fbox; l = a->lbox; for (b = 1; ; b++) { /* fill one bin */ for (i = f; i <= l; i++) i->k = FALSE; /* box not chosen */ a->iter3d = 0; a->maxfill = 0; a->miss = DIF(f,l); a->maxiter = 5*MAXITER; terminate = FALSE; n = DIF(f,l); a->mcut = 2; if (n < 15) a->mcut = 3; if (n < 10) a->mcut = 4; branch(a, f, l, n, 0); /* copy solution */ for (i = a->fsol, j = f, m = l+1; j != m; i++, j++) *j = *i; /* remove chosen boxes */ for (i = f; i <= l;) if (i->k) { i->bno = b; SWAP(i,l); l--; } else i++; /* check if finished */ if (l == f-1) break; } if (b < a->zmcut) a->zmcut = b; /* save solution */ if (a->zmcut < a->z) savesol(a, a->fbox, a->lbox, a->zmcut); } /* ====================================================================== mcut3_heuristic ====================================================================== */ /* Knapsack filling of a single bin, solved heuristically. Three different */ /* rotations of the problem are considered, and the best found solution */ /* is selected. */ void mcut3_heuristic(allinfo *a) { int i; double t1, t2; timer(&t1); a->zmcut = a->n; /* very bad lower bound */ for (i = WDIM; i <= DDIM; i++) { mcut_heuristic(a); rotate_solution(a, a->fopt, a->lopt); rotate_problem(a, a->fbox, a->lbox); } timer(&t2); a->mhtime = t2 - t1; } /* ********************************************************************** ********************************************************************** branch-and-bound for 3D bin-packing problem ********************************************************************** ********************************************************************** */ /* ====================================================================== fits ====================================================================== */ /* The routine "fitsm" checks whether a given subset of boxes fits into */ /* a single bin. To improve performance, specialized algorithms are derived */ /* for cases with two to three boxes */ boolean fits2(box *i, box *j, itype W, itype H, itype D) { /* all coordinates are initialized to zero, so just adjust changes! */ /* the 2-box solution is always guillotine cuttable */ if (i->w + j->w <= W) { j->x = i->w; return TRUE; } if (i->h + j->h <= H) { j->y = i->h; return TRUE; } if (i->d + j->d <= D) { j->z = i->d; return TRUE; } return FALSE; } boolean fits2p(box *i, box *j, itype W, itype H, itype D) { if (i->w + j->w <= W) return TRUE; if (i->h + j->h <= H) return TRUE; if (i->d + j->d <= D) return TRUE; return FALSE; } boolean fits3(box *i, box *j, box *k, itype W, itype H, itype D) { box *t; itype w, h, d, r; /* all coordinates are initialized to zero, so just adjust changes! */ /* the 3-box solution can either be cut by guillotine cuts */ for (r = 1; r <= 3; r++) { /* cut (i,j) and (k) in one of three dimensions */ w = W - k->w; h = H - k->h; d = D - k->d; if ((i->w<=w) && (j->w<=w) && fits2(i,j,w,H,D)) { k->x = w; return TRUE; } if ((i->h<=h) && (j->h<=h) && fits2(i,j,W,h,D)) { k->y = h; return TRUE; } if ((i->d<=d) && (j->d<=d) && fits2(i,j,W,H,d)) { k->z = d; return TRUE; } t = i; i = j; j = k; k = t; } /* (xi,yi,zi) = (0,0,0); (xj,yj,zj) = (wi,0,0); (xk,yk,zk) = (0,hi,dj) */ if ((i->w+j->w <= W) && (i->h+k->h <= H) && (j->d+k->d <= D)) { j->x = i->w; k->y = i->h; k->z = j->d; return TRUE; } /* (xi,yi,zi) = (0,0,0); (xj,yj,zj) = (wk,0,di); (xk,yk,zk) = (0,hi,0) */ if ((j->w+k->w <= W) && (i->h+k->h <= H) && (i->d+j->d <= D)) { j->x = k->w; j->z = i->d; k->y = i->h; return TRUE; } /* (xi,yi,zi) = (0,0,0); (xj,yj,zj) = (0,hi,dk); (xk,yk,zk) = (wi,0,0) */ if ((i->w+k->w <= W) && (i->h+j->h <= H) && (k->d+j->d <= D)) { j->y = i->h; j->z = k->d; k->x = i->w; return TRUE; } /* (xi,yi,zi) = (0,0,0); (xj,yj,zj) = (0,hi,0); (xk,yk,zk) = (wj,0,di) */ if ((j->w+k->w <= W) && (i->h+j->h <= H) && (k->d+i->d <= D)) { j->y = i->h; k->x = j->w; k->z = i->d; return TRUE; } /* (xi,yi,zi) = (0,0,0); (xj,yj,zj) = (wi,0,0); (xk,yk,zk) = (0,hj,di) */ if ((i->w+j->w <= W) && (j->h+k->h <= H) && (i->d+k->d <= D)) { j->x = i->w; k->y = j->h; k->z = i->d; return TRUE; } /* (xi,yi,zi) = (0,0,0); (xj,yj,zj) = (0,0,di); (xk,yk,zk) = (wi,hj,0) */ if ((i->w+k->w <= W) && (j->h+k->h <= H) && (i->d+j->d <= D)) { j->z = i->d; k->x = i->w; k->y = j->h; return TRUE; } return FALSE; } boolean fitsm(allinfo *a, box *t, box *k, boolean fast) { boolean fits; ntype lb; lb = bound_two(a, t, k); if (lb > 1) return FALSE; a->exfill++; fits = FALSE; fits = onebin_general(a, t, k, fast); return fits; } /* ====================================================================== onebin_decision ====================================================================== */ /* The following procedure checks whether a new box "j" fits into the the */ /* bin "bno" (together with already placed boxes in the bin). If the answer */ /* is "no" then definitly it is not possible to place the box into the bin. */ boolean onebin_decision(allinfo *a, box *j, int bno) { register box *i, *k, *m; box t[MAXBOXES]; boolean fits; int size; for (i = a->fbox, m = j, k = t-1; i != m; i++) { if (i->bno == bno) { k++; *k = *i; k->x = k->y = k->z = 0; k->ref = i; } } k++; *k = *j; k->x = k->y = k->z = 0; k->ref = j; k->k = TRUE; size = DIF(t,k); switch (size) { case 0: error("no boxes in onebin_decision"); case 1: fits = TRUE; k->x = k->y = k->z = 0; break; case 2: fits = fits2(t, k, a->W, a->H, a->D); break; case 3: fits = fits3(t, t+1, k, a->W, a->H, a->D); break; default: fits = fitsm(a, t, k, FALSE); break; } if (size <= 3) { a->subiterat++; if (a->subiterat == IUNIT) { a->subiterat = 0; a->iterat++; check_iterlimit(a->iterat, a->iterlimit); } } if (fits) { /* copy solution back */ for (i = t, m = k+1; i != m; i++) { k = i->ref; k->x = i->x; k->y = i->y; k->z = i->z; k->k = TRUE; } } return fits; } /* ====================================================================== onebin_heuristic ====================================================================== */ /* This is a heuristic version of "onebin_decision". If the answer is "yes" */ /* then a heuristic solution has been found where boxes f,..,l fit into a */ /* bin. If the answer is "no" then a filling may still be possible, but it */ /* was not found by the heuristic. */ boolean onebin_heuristic(allinfo *a, box *f, box *l) { box *i, *m; boolean fits; for (i = f, m = l+1; i != m; i++) { i->x = i->y = i->z = 0; } switch (DIF(f,l)) { case 0: error("no boxes in onebin_heuristic"); case 1: fits = TRUE; break; case 2: fits = fits2(f, l, a->W, a->H, a->D); break; case 3: fits = fits3(f, f+1, l, a->W, a->H, a->D); break; default: fits = fitsm(a, f, l, TRUE); break; } return fits; } /* ====================================================================== try_close ====================================================================== */ /* A bin may be closed if no more boxes fit into it. The present version */ /* uses a more advanced criterion: First, the set of boxes which fit into */ /* bin "bno" is derived. This is done, by testing whether each box (alone) */ /* fits together with the already placed boxes in the bin. Having derived */ /* the set of additional boxes that (individually) fits into the bin, it is */ /* tested whether a solution exists where all the additional boxes are */ /* placed in the bin. If this is the case, we have found a optimal placing */ /* of the additional boxes, and thus we may close the bin. */ boolean try_close(allinfo *a, box **curr, ntype bno, box *oldf, box **oldl, box **oldlc, ntype *oldnoc, boolean *oldclosed, int level) { register box *j, *m, *k, *r, *i, *s; register stype vol; box f[MAXBOXES]; ntype h, n, b; boolean didclose, fits; if (level > MAXCLOSE) return FALSE; i = *curr; didclose = FALSE; for (b = 1; b <= bno; b++) { if (i > a->lbox) break; if (a->closed[b]) continue; for (j = a->fbox, m = i, k = f, n = 0, vol = 0; j != m; j++) { if (j->bno == b) { *k = *j; k->ref = j; k++; n++; vol += j->vol; } } if (n == 0) error("bin with no boxes"); if (vol < a->BVOL/2) continue; for (j = i, h = 0, m = a->lbox+1; j != m; j++) { if ((j->no > a->n) || (j->no < 1)) error("bad no"); fits = onebin_decision(a, j, b); if (fits) { *k = *j; k->ref = j; k++; h++; vol += j->vol; } if (vol > a->BVOL) break; } if (vol > a->BVOL) continue; if (onebin_heuristic(a, f, k-1)) { if (!didclose) { /* take backup of table when first bin closed */ memcpy(oldclosed, a->closed, sizeof(boolean)*(bno+1)); memcpy(oldf, a->fbox, sizeof(box)*DIF(a->fbox,a->lbox)); *oldl = a->lbox; *oldlc = a->lclosed; *oldnoc = a->noc; } a->closed[b] = TRUE; s = a->lclosed; didclose = TRUE; a->noc++; if (a->noc > a->maxclose) a->maxclose = a->noc; for (j = f; j != k; j++) { r = j->ref; r->bno = b; r->k = TRUE; r->x = j->x; r->y = j->y; r->z = j->z; } for (j = k = a->fbox, m = a->lbox+1; j != m; j++) { if (j->bno == b) { s++; *s = *j; } else { *k = *j; k++; } } a->lbox = k-1; a->lclosed = s; i -= n; /* reposition current box */ } } *curr = i; return didclose; } /* ====================================================================== free_close ====================================================================== */ /* Reopen a closed bin when backtracking. */ void free_close(allinfo *a, ntype bno, box *oldf, box *oldl, box *oldlc, ntype oldnoc, boolean *oldclosed) { a->lbox = oldl; a->lclosed = oldlc; a->noc = oldnoc; memcpy(a->fbox, oldf, sizeof(box)*DIF(a->fbox,oldl)); memcpy(a->closed, oldclosed, sizeof(boolean)*(bno+1)); } /* ====================================================================== rec_binpack ====================================================================== */ /* Recursive algorithm for 3D Bin-packing Problem. In each iteration, the */ /* next box "i" is assigned to every open bin, as well as to a new bin. */ void rec_binpack(allinfo *a, box *i, int bno, ntype lb, int level) { box of[MAXBOXES], *ol, *ox; boolean ocl[MAXBOXES]; ntype b, oc; boolean more; if (bno >= a->z) return; /* used too many bins */ if (a->z == a->lb) return; /* optimal solution found */ a->subnodes++; if (a->subnodes == IUNIT) { a->subnodes = 0; a->nodes++; } check_nodelimit(a->nodes, a->nodelimit); check_iterlimit(a->iterat, a->iterlimit); check_timelimit(a->timelimit); if (stopped) return; if (i == a->lbox+1) { /* all boxes assigned, must be better solution */ savesol(a, a->fbox, a->lbox, bno); } else { more = try_close(a, &i, bno, of, &ol, &ox, &oc, ocl, level); if (i == a->lbox+1) { /* all boxes went into closed bins */ savesol(a, a->fbox, a->lbox, bno); } else { if (more) lb = a->noc + bound_two(a, a->fbox, a->lbox); if (lb < a->z) { for (b = 1; b <= bno; b++) { if (a->closed[b]) continue; /* cannot add to closed bin */ if (onebin_decision(a, i, b)) { i->bno = b; rec_binpack(a, i+1, bno, lb, level+1); i->bno = 0; } } i->bno = bno+1; i->x = i->y = i->z = 0; a->closed[i->bno] = FALSE; rec_binpack(a, i+1, bno+1, lb, level+1); i->bno = 0; } } /* restore */ if (more) free_close(a, bno, of, ol, ox, oc, ocl); } } /* ********************************************************************** ********************************************************************** Main procedure ********************************************************************** ********************************************************************** */ /* ====================================================================== clearboxes ====================================================================== */ void clearboxes(allinfo *a) { box *i, *m; for (i = a->fbox, m = a->lbox+1; i != m; i++) { i->x = i->y = i->z = i->bno = 0; i->k = FALSE; i->vol = VOL(i); } /* sort nonincreasing volume */ qsort(a->fbox, (m-a->fbox), sizeof(box), (funcptr) vcomp); } /* ====================================================================== copyboxes ====================================================================== */ void copyboxes(allinfo *a, int *w, int *h, int *d, int W, int H, int D) { box *i, *m; int k; for (i = a->fbox, m = a->lbox+1, k = 0; i != m; i++, k++) { i->no = k+1; i->w = w[k]; i->h = h[k]; i->d = d[k]; if ((w[k] < 1) || (w[k] > W)) error("bad w\n"); if ((h[k] < 1) || (h[k] > H)) error("bad h\n"); if ((d[k] < 1) || (d[k] > D)) error("bad d\n"); } clearboxes(a); } /* ====================================================================== returnboxes ====================================================================== */ void returnboxes(allinfo *a, int *x, int *y, int *z, int *bno) { box *i, *m; int k; for (i = a->fopt, m = a->lopt+1; i != m; i++) { k = i->no-1; x[k] = i->x; y[k] = i->y; z[k] = i->z; bno[k] = i->bno; } } /* ====================================================================== binpack3d ====================================================================== */ void binpack3d(int n, int W, int H, int D, int *w, int *h, int *d, int *x, int *y, int *z, int *bno, int *lb, int *ub, int nodelimit, int iterlimit, int timelimit, int *nodeused, int *iterused, int *timeused) { allinfo a; box t0[MAXBOXES], t1[MAXBOXES], t2[MAXBOXES], t3[MAXBOXES]; boolean cl[MAXBOXES]; /* start the timer */ timer(NULL); stopped = FALSE; /* copy info to a structure */ if (n+1 > MAXBOXES) error("too big instance"); a.n = n; a.W = W; a.H = H; a.D = D; a.fbox = t0; a.lbox = a.fbox + a.n - 1; a.fsol = t1; a.lsol = a.fsol + a.n - 1; a.fopt = t2; a.lopt = a.fopt + a.n - 1; a.fclosed = t3; a.lclosed = a.fclosed - 1; a.noc = 0; a.closed = cl; a.BVOL = W * (ptype) H * D; a.maxfill = 0; a.exfill = 0; a.nodelimit= 0; a.iterlimit= 0; a.timelimit= 0; a.nodes = 0; a.subnodes = 0; a.iterat = 0; a.subiterat= 0; a.didpush = 0; a.maxclose = 0; a.genertime= 0; a.robottime= 0; a.z = a.n+1; /* copy boxes to internal structure */ copyboxes(&a, w, h, d, W, H, D); /* find bounds */ a.bound0 = bound_zero(&a, a.fbox, a.lbox); a.bound1 = bound_one(&a, a.fbox, a.lbox); a.bound2 = bound_two(&a, a.fbox, a.lbox); a.lb = a.bound2; /* find heuristic solution */ dfirst3_heuristic(&a); /* initialize search limits for exact search */ a.nodelimit= nodelimit; a.iterlimit= iterlimit; a.timelimit= timelimit; /* outer tree enummeration */ clearboxes(&a); /* clear positions */ rec_binpack(&a, a.fbox, 0, a.lb, 1); timer(&(a.time)); /* check found solution */ /* checksol(&a, a.fopt, a.lopt); */ /* copy boxes back to arrays */ returnboxes(&a, x, y, z, bno); *ub = a.z; *lb = (stopped ? a.lb : a.z); *nodeused = a.nodes; *iterused = a.iterat; *timeused = a.time * 1000; } ================================================ FILE: pyshipping/__init__.py ================================================ """pyShipping contains routines related to shipping and warehousing.""" ================================================ FILE: pyshipping/addressvalidation.py ================================================ #!/usr/bin/env python # encoding: utf-8 """ addressvalidation.py - check the validity of addresses Should once integrate with http://www.deutschepost.de/dpag?tab=1&skin=hi&check=yes&lang=de_DE&xmlFile=link1015574_1021 http://www.isogmbh.de/leistungen/dataquality-management/adressvalidierung.html or http://opengeodb.hoppe-media.com/ Created by Maximillian Dornseif on 2009-09-03. Copyright (c) 2009, 2010 HUDORA. All rights reserved. """ import unittest def validate(adr, servicelevel=1): """Validates an address and returns a possibly corrected address. 'adr' should be a object conforming to the address protocol - see http://cybernetics.hudora.biz/projects/wiki/AddressProtocol 'servicelevel' can be an integer with the following values: 1 - generic validation, no money/effort should be spend on correction and suggestions 2 - TBD. returns (status, message, [corrected addresses and variants]) status can be: '10invalid' - address is for sure non deliverable in this form '20troubled' - bounced before or is unlikely to be correct - possible alternatives are returned '30ok' - likely to work '31ok' - likely to work but was corrected '40verified' - we are sure it works """ adr['land'] = adr['land'].strip() adr['plz'] = adr['plz'].strip() if adr['land'] != 'IE' and not adr['plz']: return ('10invalid', 'Postleitzahl fehlt', [adr]) if adr['land'] == 'DE' and len(adr.get('plz', '')) != 5: return ('10invalid', 'Postleitzahl fehlerhaft', [adr]) return ('30ok', '', [adr]) class AddressvalidationTests(unittest.TestCase): """Tests for the address validation facility.""" def setUp(self): """Set up test address base.""" self.address = {'name1': 'HUDORA GmbH', 'name2': 'Abt. Cybernetics', 'strasse': 'Jägerwald 13', 'land': 'DE', 'plz': '42897', 'ort': 'Remscheid', 'tel': '+49 2191 60912 0', 'fax': '+49 2191 60912 50', 'mobil': '+49 175 00000xx', 'email': 'nobody@hudora.de'} def test_good_address(self): """Test if correct addresses are considered correct.""" self.assertEqual(validate(self.address)[0], '30ok') self.assertEqual(validate(self.address)[1], '') def test_missing_zip(self): """Test if correct addresses are considered correct.""" self.address['plz'] = '' self.assertEqual(validate(self.address)[0], '10invalid') def test_short_zip(self): """Test if correct addresses are considered correct.""" self.address['plz'] = '123' self.assertEqual(validate(self.address)[0], '10invalid') def test_long_zip(self): """Test if correct addresses are considered correct.""" self.address['plz'] = '12345 Rade' self.assertEqual(validate(self.address)[0], '10invalid') if __name__ == '__main__': unittest.main() ================================================ FILE: pyshipping/binpack.py ================================================ #!/usr/bin/env python # encoding: utf-8 """ binpack.py Created by Maximillian Dornseif on 2010-08-16. Copyright (c) 2010 HUDORA. All rights reserved. """ import binpack_simple def binpack(packages, bin=None, iterlimit=5000): return binpack_simple.binpack(packages, bin, iterlimit) def test(func): import time from package import Package fd = open('testdata.txt') vorher = 0 nachher = 0 start = time.time() counter = 0 for line in fd: counter += 1 if counter > 450: break packages = [Package(pack) for pack in line.strip().split()] if not packages: continue bins, rest = func(packages) if rest: print "invalid data", rest, line else: vorher += len(packages) nachher += len(bins) print time.time() - start, print vorher, nachher, float(nachher) / vorher * 100 if __name__ == '__main__': print "py", test(binpack) import time from pyshipping.package import Package ================================================ FILE: pyshipping/binpack_simple.py ================================================ #!/usr/bin/env python # encoding: utf-8 """ binpack_simple.py This code implemnts 3D bin packing in pure Python Bin packing in this context is calculating the best way to store a number of differently sized boxes in a number of fixed sized "bins". It is what usually happens in a Warehouse bevore shipping. The Algorithm has a simple fit first approach, but can archive relative good results because it tries different rectangular rotations of the packages. Since the Algorithm can't interate over all possible combinations we use a heuristic approach. For a few dozen packages it reaches adaequate runtime. Below are the results calculated about a set of 500 real world packing problems. Binsize Runtime Recuction in shipped Packages 600x400x400 31.5993559361 4970 2033 40.9054325956 600x445x400 31.5596890450 4970 1854 37.3038229376 600x500x400 29.1432909966 4970 1685 33.9034205231 On the datasets we operate on we can archive comparable preformance to academic higly optimized C code like David Pisinger's 3bpp: Runtime Recuction in shipped Packages py 11.3468761444 2721 1066 39.1767732451 3bpp 9.95857691765 2721 1086 39.9117971334 The Python implementation is somewhat slower but can archive slightly better packing results on our datasets. Created by Maximillian Dornseif on 2010-08-14. Copyright (c) 2010 HUDORA. All rights reserved. """ import time import random def packstrip(bin, p): """Creates a Strip which fits into bin. Returns the Packages to be used in the strip, the dimensions of the strip as a 3-tuple and a list of "left over" packages. """ # This code is somewhat optimized and somewhat unreadable s = [] # strip r = [] # rest ss = sw = sl = 0 # stripsize bs = bin.heigth # binsize sapp = s.append # speedup rapp = r.append # speedup ppop = p.pop # speedup while p and (ss <= bs): n = ppop(0) nh, nw, nl = n.size if ss + nh <= bs: ss += nh sapp(n) if nw > sw: sw = nw if nl > sl: sl = nl else: rapp(n) return s, (ss, sw, sl), r + p def packlayer(bin, packages): strips = [] layersize = 0 layerx = 0 layery = 0 binsize = bin.width while packages: strip, (sizex, stripsize, sizez), rest = packstrip(bin, packages) if layersize + stripsize <= binsize: packages = rest if not strip: # we were not able to pack anything break layersize += stripsize layerx = max([sizex, layerx]) layery = max([sizez, layery]) strips.extend(strip) else: # Next Layer please packages = strip + rest break return strips, (layerx, layersize, layery), packages def packbin(bin, packages): packages.sort() layers = [] contentheigth = 0 contentx = 0 contenty = 0 binsize = bin.length while packages: layer, (sizex, sizey, layersize), rest = packlayer(bin, packages) if contentheigth + layersize <= binsize: packages = rest if not layer: # we were not able to pack anything break contentheigth += layersize contentx = max([contentx, sizex]) contenty = max([contenty, sizey]) layers.extend(layer) else: # Next Bin please packages = layer + rest break return layers, (contentx, contenty, contentheigth), packages def packit(bin, originalpackages): packedbins = [] packages = sorted(originalpackages) while packages: packagesinbin, (binx, biny, binz), rest = packbin(bin, packages) if not packagesinbin: # we were not able to pack anything break packedbins.append(packagesinbin) packages = rest # we now have a result, try to get a better result by rotating some bins return packedbins, rest # In newer Python versions these van be imported: # from itertools import permutations def product(*args, **kwds): # product('ABCD', 'xy') --> Ax Ay Bx By Cx Cy Dx Dy # product(range(2), repeat=3) --> 000 001 010 011 100 101 110 111 pools = map(tuple, args) * kwds.get('repeat', 1) result = [[]] for pool in pools: result = [x + [y] for x in result for y in pool] for prod in result: yield tuple(prod) def permutations(iterable, r=None): pool = tuple(iterable) n = len(pool) r = n if r is None else r for indices in product(range(n), repeat=r): if len(set(indices)) == r: yield tuple(pool[i] for i in indices) class Timeout(Exception): pass def allpermutations_helper(permuted, todo, maxcounter, callback, bin, bestpack, counter): if not todo: return counter + callback(bin, permuted, bestpack) else: others = todo[1:] thispackage = todo[0] for dimensions in set(permutations((thispackage[0], thispackage[1], thispackage[2]))): thispackage = Package(dimensions, nosort=True) if thispackage in bin: counter = allpermutations_helper(permuted + [thispackage], others, maxcounter, callback, bin, bestpack, counter) if counter > maxcounter: raise Timeout('more than %d iterations tries' % counter) return counter def trypack(bin, packages, bestpack): bins, rest = packit(bin, packages) if len(bins) < bestpack['bincount']: bestpack['bincount'] = len(bins) bestpack['bins'] = bins bestpack['rest'] = rest if bestpack['bincount'] < 2: raise Timeout('optimal solution found') return len(packages) def allpermutations(todo, bin, iterlimit=5000): random.seed(1) random.shuffle(todo) bestpack = dict(bincount=len(todo) + 1) try: # First try unpermuted trypack(bin, todo, bestpack) # now try permutations allpermutations_helper([], todo, iterlimit, trypack, bin, bestpack, 0) except Timeout: pass return bestpack['bins'], bestpack['rest'] def binpack(packages, bin=None, iterlimit=5000): """Packs a list of Package() objects into a number of equal-sized bins. Returns a list of bins listing the packages within the bins and a list of packages which can't be packed because they are to big.""" if not bin: bin = Package("600x400x400") return allpermutations(packages, bin, iterlimit) def test(): fd = open('testdata.txt') vorher = 0 nachher = 0 start = time.time() for line in fd: packages = [Package(pack) for pack in line.strip().split()] if not packages: continue bins, rest = binpack(packages) if rest: print "invalid data", rest, line else: vorher += len(packages) nachher += len(bins) print time.time() - start, print vorher, nachher, float(nachher) / vorher * 100 if __name__ == '__main__': import cProfile cProfile.run('test()') from pyshipping.package import Package ================================================ FILE: pyshipping/carriers/__init__.py ================================================ """Function for specific freight carriers.""" ================================================ FILE: pyshipping/carriers/dpd/__init__.py ================================================ """Functions specific to DPD/Geopost.""" ================================================ FILE: pyshipping/carriers/dpd/georoute.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """ georoute.py - get DPD related routng information Originally coded by md, cleand up and extended by jmv and then again reworked by md. Copyright 2006, 2007 HUDORA GmbH. Published under a BSD License. """ import os import os.path import gzip import logging import sqlite3 ROUTETABLES_BASE = os.path.join(os.path.split(os.path.abspath(__file__))[0], 'georoutetables') ROUTES_DB_BASE = '/tmp/dpdroutes' # Quelle: http://de.wikipedia.org/wiki/Liste_der_Kfz-Nationalitätszeichen ISO2CAR = { 'AT': 'A', 'BE': 'B', 'FR': 'F', } class InvalidFormatError(Exception): """Invalid input file format.""" pass class GeorouteException(Exception): """Base class for all routing exceptions""" pass class CountryError(GeorouteException): """Unknown country.""" pass class DepotError(GeorouteException): """Unknown depot.""" pass class ServiceError(GeorouteException): """Unknown service.""" pass class TranslationError(GeorouteException): """Cannot translate city and country to postcode.""" pass class RoutingDepotError(GeorouteException): """Unknown routing depot.""" pass class NoRouteError(GeorouteException): """Route not found.""" pass class Parcel(object): """Parcel destination data.""" # depreciated def __init__(self, depot='142', service='101', country='DE', city=None, postcode=None): import warnings warnings.warn("Parcel() is deprecated", DeprecationWarning, stacklevel=2) self.service = service self.country = country self.city = city self.postcode = postcode class Destination(object): """Parcel destination data.""" def __init__(self, country='DE', postcode=None, city=None, service='101'): self.service = service self.country = country self.city = city self.postcode = postcode class Route: """Output of the routing algorithm.""" def __init__(self, d_depot, o_sort, d_sort, grouping_priority, barcode_id, iata_code, service_text, service_mark, country, serviceinfo, countrynum, routingtable_version, postcode): self.d_depot = d_depot self.o_sort = o_sort self.d_sort = d_sort self.grouping_priority = grouping_priority self.barcode_id = barcode_id self.iata_code = iata_code self.service_text = service_text self.service_mark = service_mark self.country = country self.serviceinfo = serviceinfo self.countrynum = countrynum self.routingtable_version = routingtable_version self.postcode = postcode def __unicode__(self): output = u"""Output parameters: Country: %s D-Depot: %s O-Sort: %s D-Sort: %s Grouping priority: %s Barcode ID: %s ITA Code: %s Service Text: %s""" % (self.country, self.d_depot, self.o_sort, self.d_sort, self.grouping_priority, self.barcode_id, self.iata_code, self.service_text) if self.service_mark: output += "\nService Mark: %s" % self.service_mark if self.iata_code: output += "\nIATA Code: %s" % self.iata_code if self.serviceinfo: output += "\nService Info: %s" % self.serviceinfo return output def __repr__(self): return repr(vars(self)) def routingdata(self): return {'o_sort': self.o_sort, 'd_sort': self.d_sort, 'd_depot': self.d_depot, 'country': self.country, 'service_text': self.service_text, 'serviceinfo': self.serviceinfo} def _readfile(filename): """Read file line-by-line skipping comments.""" if os.path.exists(filename + '.gz'): fhandle = gzip.GzipFile(filename + '.gz') else: fhandle = file(filename) for line in fhandle: line = line.strip().decode('latin1') if line.startswith('#'): continue yield line.split('|') class RouteData(object): """More convenient representation of the georoute data.""" def __init__(self, routingdepot='0142'): """Routingdepot the depot from where you are sending.""" self.routingdepot = routingdepot self.routingdepotgroups = '' self.routingdepotcountry = '' services = os.path.join(ROUTETABLES_BASE, 'SERVICE') self.version = None for line in file(services): if line.startswith('#Version: '): self.version = line.split(':')[1].strip() break if self.version is None: raise InvalidFormatError("There's no version in the SERVICE file") self.countries = {} for line in _readfile(os.path.join(ROUTETABLES_BASE, 'COUNTRY')): isonum, isoname = line[:2] self.countries[isoname.upper()] = isonum self.depots = {} for line in _readfile(os.path.join(ROUTETABLES_BASE, 'DEPOTS')): geopostdepotnumber = line[0] self.depots[geopostdepotnumber] = tuple(line) self.services = {} for line in _readfile(os.path.join(ROUTETABLES_BASE, 'SERVICE')): servicecode = line[0] self.services[servicecode] = tuple(line) self.serviceinfo = {} for line in _readfile(os.path.join(ROUTETABLES_BASE, 'SERVICEINFO.DE')): servicecode = line[0] self.serviceinfo[servicecode] = line[1] filename = ROUTES_DB_BASE + ('-%s-%s.db' % (routingdepot, self.version)) self.db = sqlite3.connect(filename) self.read_depots(ROUTETABLES_BASE) self.read_locations(ROUTETABLES_BASE) self.read_routes(ROUTETABLES_BASE) def read_depots(self, path): """Read DEPOTS file and save all the information in a SQLite database.""" c = self.db.cursor() c.execute("""SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = 'depots'""") if not c.fetchone()[0]: logging.info("regenerating depots table") c.execute("""CREATE TABLE depots (DepotNumber TEXT PRIMARY KEY, IATACode TEXT, GroupId TEXT, Name1 TEXT, Name2 TEXT, Address1 TEXT, Address2 TEXT, Postcode TEXT, CityName TEXT, Country TEXT, Phone TEXT, Fax TEXT, Mail TEXT, Web TEXT)""") for line in _readfile(os.path.join(path, 'DEPOTS')): c.execute("""INSERT INTO depots VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", line[:14]) if line[0] == self.routingdepot: self.routingdepotgroups = line[2] self.routingdepotgrouplist = line[2].split(',') self.routingdepotcountry = line[9] c.execute('VACUUM;') def read_locations(self, path): """Read LOCATION file and save all the information in a SQLite database.""" c = self.db.cursor() c.execute("""SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='location'""") if not c.fetchone()[0]: logging.info("regenerating location table") c.execute("""CREATE TABLE location (Area TEXT, City TEXT, Country TEXT, Postcode TEXT)""") for line in _readfile(os.path.join(path, 'LOCATION.DE')): c.execute('INSERT INTO location VALUES (?,?,?,?)', line[:4]) c.execute('VACUUM;') def read_routes(self, path): """Read ROUTES file and save all the information in a SQLite database.""" # self.db = sqlite3.connect(ROUTES_DB) c = self.db.cursor() c.execute("""SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='routes'""") if not c.fetchone()[0]: logging.info("regenerating routes table") c.execute("""CREATE TABLE routes (id INTEGER PRIMARY KEY, DestinationCountry TEXT, BeginPostCode TEXT, EndPostCode TEXT, ServiceCodes TEXT, RoutingPlaces TEXT, SendingDate TEXT, OSort TEXT, DDepot TEXT, GroupingPriority TEXT, DSort TEXT, BarcodeID TEXT)""") c.execute("""SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='routedepots'""") if not c.fetchone()[0]: logging.info("regenerating routedepots table") c.execute("""CREATE TABLE routedepots (route INTEGER, depot TEXT)""") c.execute("PRAGMA synchronous=OFF;") c.execute("PRAGMA temp_store=MEMORY;") i = 1 for line in _readfile(os.path.join(path, 'ROUTES')): services = self.expand_services(line[3]) c.execute('INSERT INTO routes VALUES (?,?,?,?,?,?,?,?,?,?,?,?)', [i] + line[:3] + [services] + line[4:-1]) self.expand_depots(i, line[4], c) i += 1 c.execute("CREATE INDEX routes_DestinationCountry ON routes(DestinationCountry)") c.execute("CREATE INDEX routes_BeginPostCode ON routes(BeginPostCode)") c.execute("CREATE INDEX routes_EndPostCode ON routes(EndPostCode)") c.execute("CREATE INDEX routedepots_route ON routedepots(route)") c.execute("CREATE INDEX routedepots_depot ON routedepots(depot)") c.execute('VACUUM;') # also commits the database def expand_services(self, services): """Expand services list.""" services_list = [] for service in services.split(','): if len(service) > 4: start = int(service[1:4]) end = int(service[4:]) for i in range(start, end + 1): services_list.append(unicode(i)) else: services_list.append(service[1:]) return ','.join(services_list) def expand_depots(self, route, depots, c): """Parse depots list and generate route->depots relationship.""" # but only four "our" depot. # if you change the self.routingdepot, you have to rebuild the database if depots == '': c.execute("""INSERT INTO routedepots(route, depot) VALUES (?, ?)""", (route, depots)) return for depot in depots.split(','): if depot.startswith('C'): if depot[1:] == self.routingdepotcountry: c.execute("""INSERT INTO routedepots(route, depot) VALUES(?, ?)""", (route, self.routingdepot)) elif depot.startswith('D'): if len(depot) > 5: start = int(depot[1:5]) end = int(depot[5:]) for i in range(start, end + 1): if ("%04d" % i) == self.routingdepot: c.execute("""INSERT INTO routedepots(route, depot) VALUES(?, ?)""", (route, self.routingdepot)) else: if depot[1:5] == self.routingdepot: c.execute("""INSERT INTO routedepots(route, depot) VALUES(?, ?)""", (route, depot[1:5])) elif depot.startswith('G'): if depot[1:] in self.routingdepotgroups: c.execute("INSERT INTO routedepots(route, depot) VALUES(?, ?)", (route, self.routingdepot)) else: raise InvalidFormatError("Unable to parse depot '%s'" % depot) def get_countrynum(self, isoname): """Return country ISO code.""" if not isoname.upper() in self.countries: raise CountryError("Country %s unknown" % isoname) return self.countries[isoname.upper()] def get_depot(self, depotnumber): """Return depot.""" if not depotnumber in self.depots: raise DepotError("Depot %s unknown" % depotnumber) return self.depots[depotnumber] def get_service(self, servicecode): """Return service.""" if not servicecode in self.services: raise ServiceError("Service %s unknown" % servicecode) return self.services[servicecode] def get_servicetext(self, servicecode): """Return service info to be printed on label.""" if not servicecode in self.serviceinfo: return '' return self.serviceinfo[servicecode] def translate_location(self, city, country): """Return postcode for given city and country.""" cur = self.db.cursor() cur.execute("SELECT Postcode FROM location WHERE City=? AND Country=?", (city, country)) rows = cur.fetchall() if not rows: raise TranslationError("Cannot find postcode for location %s, %s" % (city, country)) return rows[0][0] class Router(object): """Routes parcels.""" def __init__(self, data): self.route_data = data self.db = self.route_data.db def route(self, parcel): """Find route.""" self.current_subset = [] self.conditions = ['1=1'] self.cleanup_postcode(parcel) self.select_country(parcel) if parcel.postcode is None: parcel.postcode = self.route_data.translate_location(parcel.city, parcel.country) self.subsetstack.append(self.subset) self.select_postcode(parcel) self.select_service(parcel) self.select_depot(parcel) # Sending date is not used yet, according to documentation # If there are several routes, always use the first one. # In prior versions, an exception was raised instead. if len(self.current_subset) >= 1: rows = self.select_routes("1=1") (service_text, service_mark) = self.route_data.get_service(parcel.service)[1:3] depot = self.route_data.get_depot(rows[0][8]) iata_code = depot[1] country = depot[9] if not country: country = parcel.country serviceinfo = self.route_data.get_servicetext(parcel.service) return Route(rows[0][8], rows[0][7], rows[0][10], rows[0][9], rows[0][11], iata_code, service_text, service_mark, country, serviceinfo, self.route_data.get_countrynum(country), self.route_data.version, parcel.postcode) raise NoRouteError("No route found for %r|%r|%r" % \ (parcel.country, parcel.postcode, parcel.service)) def add_condition(self, condition): self.conditions.append(condition) def select_routes(self, condition, params=()): """Find routes matching condition and currently selected subset of the routes table. If routes are found, save their ids for narrowing future searches. If no routes are found, do not change current subset. """ subsetcondition = ' AND '.join(self.conditions) cur = self.db.cursor() cur.execute("SELECT * FROM routes WHERE %s AND %s" % (subsetcondition, condition), params) rows = cur.fetchall() # Save matched rows if there were any results if rows: self.add_condition(condition) self.current_subset = [unicode(row[0]) for row in rows] return rows def select_country(self, parcel): """Select all routes with the given country.""" rows = self.select_routes("DestinationCountry='%s'" % (parcel.country.upper().replace("'", ''), )) if not rows: raise CountryError("Country %s unknown" % parcel.country) def cleanup_postcode(self, parcel): """Removes spaces and country prefixes from postcodes.""" if not parcel.postcode: return parcel.postcode = parcel.postcode.replace(' ', '').strip() if parcel.postcode.startswith('-'): parcel.postcode = parcel.postcode[1:] self.cleanup_postcode(parcel) if parcel.postcode.upper().startswith(parcel.country.upper()): parcel.postcode = parcel.postcode[len(parcel.country):] self.cleanup_postcode(parcel) if parcel.country.upper() in ISO2CAR: if parcel.postcode.upper().startswith(ISO2CAR[parcel.country.upper()]): parcel.postcode = parcel.postcode[len(ISO2CAR[parcel.country.upper()]):] self.cleanup_postcode(parcel) if parcel.country.upper() == 'DE' and parcel.postcode.upper().startswith('CH-'): parcel.country = 'CH' parcel.postcode = parcel.postcode[2:] self.cleanup_postcode(parcel) if parcel.country.upper() == 'DE' and parcel.postcode.upper().startswith('BE-'): parcel.country = 'BE' parcel.postcode = parcel.postcode[2:] self.cleanup_postcode(parcel) if parcel.country.upper() == 'DE' and parcel.postcode.upper().startswith('B-'): parcel.country = 'BE' parcel.postcode = parcel.postcode[1:] self.cleanup_postcode(parcel) if parcel.country.upper() == 'DE' and parcel.postcode.upper().startswith('AT-'): parcel.country = 'AT' parcel.postcode = parcel.postcode[2:] self.cleanup_postcode(parcel) if parcel.country.upper() == 'DE' and parcel.postcode.upper().startswith('A-'): parcel.country = 'AT' parcel.postcode = parcel.postcode[1:] self.cleanup_postcode(parcel) def select_postcode(self, parcel): """Select all routes matching the given postcode.""" # direct match rows = self.select_routes("BeginPostCode='%s'" % parcel.postcode.replace("'", '')) if not rows: # range rows = self.select_routes("BeginPostCode<='%s' AND EndPostCode>='%s'" % (parcel.postcode.replace("'", ''), parcel.postcode.replace("'", ''))) if not rows: # catch all rows = self.select_routes("BeginPostCode=''") if not rows: raise NoRouteError("Postcode %r|%r unknown" % (parcel.country, parcel.postcode)) def select_service(self, parcel): """Select all routes with the given service code.""" # we have to redo postcode query as a backoff strategy self.conditions.pop() postcodequeries = ["BeginPostCode='%s'" % parcel.postcode.replace("'", ''), "BeginPostCode<='%s' AND EndPostCode>='%s'" % (parcel.postcode.replace("'", ''), parcel.postcode.replace("'", '')), "BeginPostCode=''"] for postcodequery in postcodequeries: rows = self.select_routes("%s AND ServiceCodes LIKE '%%%s%%'" % (postcodequery, parcel.service)) if not rows: # catch all rows = self.select_routes("%s AND ServiceCodes = ''" % (postcodequery)) if rows: break if not rows: raise ServiceError("No route for service found %r|%r|%r unknown" % \ (parcel.country, parcel.postcode, parcel.service)) def select_depot(self, parcel): """Select all routes with the given depot.""" subset = "route IN (%s)" % ','.join([unicode(route) for route in self.current_subset]) cur = self.db.cursor() cur.execute("SELECT route FROM routedepots WHERE depot=%s AND %s" % (self.route_data.routingdepot, subset)) rows = cur.fetchall() if not rows: cur.execute("SELECT route FROM routedepots WHERE %s" % (subset)) rows = cur.fetchall() if not rows: raise RoutingDepotError("No route found for %r|%r|%r|%r|%r" % \ (parcel.country, parcel.postcode, parcel.service, self.route_data.routingdepot, subset)) self.current_subset = [unicode(row[0]) for row in rows] def get_route_without_cache(country=None, postcode=None, city=None, servicecode='101'): router = Router(RouteData()) return router.route(Destination(country, postcode, city)) def get_route(country=None, postcode=None, city=None, servicecode='101'): # this includes somewhat overly complex caching filename = ROUTES_DB_BASE + ('_cache.db') cache_db = sqlite3.connect(filename, isolation_level=None) cur = cache_db.cursor() # ensure table exists cur.execute("""SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = 'routing_cache'""") if not cur.fetchone()[0]: logging.info("regenerating cache table") cur.execute("""CREATE TABLE routing_cache (country_postcode_servicecode TEXT PRIMARY KEY, d_depot TEXT, o_sort TEXT, d_sort TEXT, grouping_priority TEXT, barcode_id TEXT, iata_code TEXT, service_text TEXT, service_mark TEXT, country TEXT, serviceinfo TEXT, countrynum TEXT, routingtable_version TEXT, postcode TEXT )""") # check if entry is cached cur.execute("SELECT * FROM routing_cache WHERE country_postcode_servicecode='%s'" % ("%s_%s_%s" % (country, postcode, servicecode))) rows = cur.fetchall() if not rows: # nothing found route = get_route_without_cache(country, postcode, city, servicecode) cur.execute("""INSERT INTO routing_cache VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", ("%s_%s_%s" % (country, postcode, servicecode), route.d_depot, route.o_sort, route.d_sort, route.grouping_priority, route.barcode_id, route.iata_code, route.service_text, route.service_mark, route.country, route.serviceinfo, route.countrynum, route.routingtable_version, route.postcode)) # For some reason closing the cache generated occasionally runtime errors. # cache_db.close() else: if len(rows) > 1: raise RuntimeError("to many cache hits!") route = Route(*rows[0][1:]) return route # compability layer to old georoute code prior to huLOG revision 1710 def find_route(depot, servicecode, land, plz): """Legacy method - to be removed.""" import warnings warnings.warn("georoute.find_route() is deprecated use get_route() instead", DeprecationWarning, stacklevel=2) if unicode(depot) != '0142': raise RuntimeError("wrong depot") return get_route(unicode(land), unicode(plz), servicecode=unicode(servicecode)) ================================================ FILE: pyshipping/carriers/dpd/georoute_test.py ================================================ #!/usr/bin/env python # -*- encoding: utf-8 -*- """Test routing resolver for DPD. Coded by jmv, extended by md""" import time import unittest from pyshipping.carriers.dpd.georoute import get_route, get_route_without_cache from pyshipping.carriers.dpd.georoute import RouteData, Router, Destination from pyshipping.carriers.dpd.georoute import ServiceError, CountryError, TranslationError class TestCase(unittest.TestCase): """Provide sophisticated dictionary comparision.""" def assertDicEq(self, dict1, dict2): """Asserts if two dicts are unequal. Raise an Exception which mentions the different entries of those dicts. """ if dict1 != dict2: difference = set() for key, value in dict1.items(): if dict2.get(key) != value: difference.add((key, value, dict2.get(key))) for key, value in dict2.items(): if dict1.get(key) != value: difference.add((key, dict1.get(key), value)) raise self.failureException, \ ('%r != %r: %s' % (dict1, dict2, list(difference))) class RouteDataTest(TestCase): def setUp(self): self.data = RouteData() self.db = self.data.db def test_version(self): self.assertEqual(self.data.version, '20110905') def test_get_country(self): self.assertRaises(CountryError, self.data.get_countrynum, 'URW') self.assertEqual(self.data.get_countrynum('JP'), '392') self.assertEqual(self.data.get_countrynum('DE'), '276') self.assertEqual(self.data.get_countrynum('de'), '276') def test_read_depots(self): c = self.db.cursor() c.execute("""SELECT * FROM depots WHERE DepotNumber=?""", ('0015', )) rows = c.fetchall() self.assertEqual(1, len(rows)) self.assertEqual((u'0015', u'', u'', u'Betriebsgesellschaft DPD GmbH', u'', u'Otto-Hahn-Strasse 5', u'', u'59423', u'Unna', u'DE', u'+49-(0) 23 03-8 88-0', u'+49-(0) 23 03-8 88-31', u'', u''), rows[0]) def test_expand_depots(self): c = self.db.cursor() c.execute("""SELECT id FROM routes WHERE DestinationCountry='DE' AND BeginPostCode='42477'""") rows = c.fetchall() # Interestingly we sometimes get two routes # self.assertEqual(1, len(rows)) route = rows[0][0] c.execute("SELECT depot FROM routedepots WHERE route=?", (route, )) rows = c.fetchall() self.assertEqual(1, len(rows)) def test_get_service(self): self.assertEqual(self.data.get_service('180'), ('180', 'AM1-NO', '', '022,160', '')) self.assertRaises(ServiceError, self.data.get_service, '100000') def test_get_servicetext(self): text = self.data.get_servicetext('185') self.assertEqual('DPD 10:00 Unfrei / ex works', text) def test_translate_location(self): self.assertEqual('1', self.data.translate_location('Dublin', 'IE')) self.assertRaises(TranslationError, self.data.translate_location, 'Cahir', 'IE') class RouterTest(TestCase): def setUp(self): self.data = RouteData() self.router = Router(self.data) def test_known_routes_de(self): route = self.router.route(Destination(postcode='42477')) self.assertDicEq(route.routingdata(), {'d_depot': u'0142', 'serviceinfo': '', 'country': u'DE', 'd_sort': u'65', 'o_sort': u'42', 'service_text': u'D'}) route = self.router.route(Destination(postcode='42897')) self.assertDicEq(route.routingdata(), {'d_depot': '0142', 'serviceinfo': '', 'country': 'DE', 'd_sort': '15', 'o_sort': '42', 'service_text': 'D'}) route = self.router.route(Destination(postcode='53111')) self.assertDicEq(route.routingdata(), {'d_depot': '0150', 'serviceinfo': '', 'country': 'DE', 'd_sort': u'205', 'o_sort': '50', 'service_text': 'D'}) route = self.router.route(Destination(postcode='53111', country='DE')) self.assertDicEq(route.routingdata(), {'d_depot': '0150', 'serviceinfo': '', 'country': 'DE', 'd_sort': u'205', 'o_sort': '50', 'service_text': 'D'}) route = self.router.route(Destination('DE', '53111')) self.assertDicEq(route.routingdata(), {'d_depot': '0150', 'serviceinfo': '', 'country': 'DE', 'd_sort': u'205', 'o_sort': '50', 'service_text': 'D'}) route = self.router.route(Destination('DE', '53111', city='Bonn')) self.assertDicEq(route.routingdata(), {'d_depot': '0150', 'serviceinfo': '', 'country': 'DE', 'd_sort': u'205', 'o_sort': '50', 'service_text': 'D'}) route = self.router.route(Destination('DE', '53111', 'Bonn')) self.assertDicEq(route.routingdata(), {'d_depot': '0150', 'serviceinfo': '', 'country': 'DE', 'd_sort': u'205', 'o_sort': '50', 'service_text': 'D'}) def test_known_routes_world(self): route = self.router.route(Destination(postcode='66400', country='FR')) self.assertDicEq(route.routingdata(), {'d_depot': u'0470', 'serviceinfo': '', 'country': u'FR', 'd_sort': u'U50', 'o_sort': u'16', 'service_text': u'D'}) route = self.router.route(Destination('FR', '66400', 'Ceret')) self.assertDicEq(route.routingdata(), {'d_depot': '0470', 'serviceinfo': '', 'country': 'FR', 'd_sort': 'U50', 'o_sort': '16', 'service_text': 'D'}) route = self.router.route(Destination('BE', '3960', 'Bree/Belgien')) self.assertDicEq(route.routingdata(), {'d_depot': '0532', 'serviceinfo': '', 'country': 'BE', 'd_sort': u'A353', 'o_sort': '52', 'service_text': 'D'}) route = self.router.route(Destination('CH', '6005', 'Luzern')) self.assertDicEq(route.routingdata(), {'d_depot': '0616', 'serviceinfo': '', 'country': 'CH', 'd_sort': '40', 'o_sort': '78', 'service_text': 'D'}) route = self.router.route(Destination('AT', '1210', 'Wien')) self.assertDicEq(route.routingdata(), {'d_depot': '0622', 'serviceinfo': '', 'country': 'AT', 'd_sort': '10', 'o_sort': '62', 'service_text': 'D'}) route = self.router.route(Destination('AT', '4820', 'Bad Ischl')) self.assertDicEq(route.routingdata(), {'d_depot': '0624', 'serviceinfo': '', 'country': 'AT', 'd_sort': '63', 'o_sort': '62', 'service_text': 'D'}) route = self.router.route(Destination('AT', '7400', 'Oberwart')) self.assertDicEq(route.routingdata(), {'d_depot': '0628', 'serviceinfo': '', 'country': 'AT', 'd_sort': u'3270', 'o_sort': '62', 'service_text': 'D'}) route = self.router.route(Destination('AT', '4400', 'Steyr')) self.assertDicEq(route.routingdata(), {'d_depot': '0624', 'serviceinfo': '', 'country': 'AT', 'd_sort': '70', 'o_sort': '62', 'service_text': 'D'}) route = self.router.route(Destination('AT', '1220', 'Wien')) self.assertDicEq(route.routingdata(), {'d_depot': '0622', 'serviceinfo': '', 'country': 'AT', 'd_sort': '30', 'o_sort': '62', 'service_text': 'D'}) route = self.router.route(Destination('AT', '6890', 'Lustenau')) self.assertDicEq(route.routingdata(), {'d_depot': '0627', 'serviceinfo': '', 'country': 'AT', 'd_sort': '01', 'o_sort': '62', 'service_text': 'D'}) route = self.router.route(Destination('BE', '3520', 'ZONHOVEN')) self.assertDicEq(route.routingdata(), {'d_depot': u'0532', 'serviceinfo': '', 'country': u'BE', 'd_sort': u'A369', 'o_sort': u'52', 'service_text': u'D'}) route = self.router.route(Destination('BE', '4890', 'Thimister')) self.assertDicEq(route.routingdata(), {'d_depot': '0532', 'serviceinfo': '', 'country': 'BE', 'd_sort': u'B326', 'o_sort': '52', 'service_text': 'D'}) route = self.router.route(Destination('CH', '8305', 'Dietlikon')) self.assertDicEq(route.routingdata(), {'d_depot': '0615', 'serviceinfo': '', 'country': 'CH', 'd_sort': '77', 'o_sort': '78', 'service_text': 'D'}) route = self.router.route(Destination('CH', '4051', 'Basel')) self.assertDicEq(route.routingdata(), {'d_depot': '0610', 'serviceinfo': '', 'country': 'CH', 'd_sort': u'16', 'o_sort': '78', 'service_text': 'D'}) route = self.router.route(Destination('CH', '8808', 'Pfffikon')) self.assertDicEq(route.routingdata(), {'d_depot': '0615', 'serviceinfo': '', 'country': 'CH', 'd_sort': '71', 'o_sort': '78', 'service_text': 'D'}) route = self.router.route(Destination('DK', '9500', 'Hobro')) self.assertDicEq(route.routingdata(), {'d_depot': '0504', 'serviceinfo': '', 'country': 'DK', 'd_sort': u'405', 'o_sort': '20', 'service_text': 'D'}) # Lichtenstein is routed via CH route = self.router.route(Destination('LI', '8399', 'Windhof / Luxembourg')) self.assertDicEq(route.routingdata(), {'d_depot': '0617', 'serviceinfo': '', 'country': 'CH', 'd_sort': '', 'o_sort': '78', 'service_text': 'D'}) route = self.router.route(Destination('LI', '9495', 'Triesen')) self.assertDicEq(route.routingdata(), {'d_depot': '0617', 'serviceinfo': '', 'country': 'CH', 'd_sort': '', 'o_sort': '78', 'service_text': 'D'}) route = self.router.route(Destination('LI', '8440', 'Steinfort')) self.assertDicEq(route.routingdata(), {'d_depot': '0617', 'serviceinfo': '', 'country': 'CH', 'd_sort': '', 'o_sort': '78', 'service_text': 'D'}) route = self.router.route(Destination('CZ', '41742', 'Krupka 1')) self.assertDicEq(route.routingdata(), {'d_depot': '1380', 'serviceinfo': '', 'country': 'CZ', 'd_sort': '21', 'o_sort': '10', 'service_text': 'D'}) route = self.router.route(Destination('ES', '28802', 'Alcala de Henares (Madrid)')) self.assertDicEq(route.routingdata(), {'d_depot': '0728', 'serviceinfo': '', 'country': 'ES', 'd_sort': '01', 'o_sort': '16', 'service_text': 'D'}) route = self.router.route(Destination('ES', '28010', 'Madrid')) self.assertDicEq(route.routingdata(), {'d_depot': '0728', 'serviceinfo': '', 'country': 'ES', 'd_sort': '01', 'o_sort': '16', 'service_text': 'D'}) route = self.router.route(Destination('FR', '84170', 'MONTEUX')) self.assertDicEq(route.routingdata(), {'d_depot': '0447', 'serviceinfo': '', 'country': 'FR', 'd_sort': u'S65', 'o_sort': '16', 'service_text': 'D'}) route = self.router.route(Destination('FR', '91044', 'Evry Cedex')) self.assertDicEq(route.routingdata(), {'d_depot': u'0408', 'serviceinfo': '', 'country': u'FR', 'd_sort': u'S61', 'o_sort': u'50', 'service_text': u'D'}) def test_difficult_routingdepots(self): route = self.router.route(Destination('AT', '3626', 'Hnibach')) self.assertDicEq(route.routingdata(), {'d_depot': u'0623', 'serviceinfo': u'', 'country': 'AT', 'd_sort': u'01', 'o_sort': u'62', 'service_text': u'D'}) route = self.router.route(Destination('AT', '8225', 'Pllau')) self.assertDicEq(route.routingdata(), {'d_depot': u'0628', 'serviceinfo': u'', 'country': 'AT', 'd_sort': u'1290', 'o_sort': u'62', 'service_text': u'D'}) route = self.router.route(Destination('AT', '5020', 'Salzburg')) self.assertDicEq(route.routingdata(), {'d_depot': u'0625', 'serviceinfo': u'', 'country': 'AT', 'd_sort': u'1000', 'o_sort': u'62', 'service_text': u'D'}) route = self.router.route(Destination('SE', '65224', 'Karlstad')) self.assertDicEq(route.routingdata(), {'d_depot': u'0307', 'serviceinfo': u'', 'country': 'SE', 'd_sort': u'01', 'o_sort': u'20', 'service_text': u'D'}) route = self.router.route(Destination('AT', '2734', 'Buchberg/Schneeberg')) self.assertDicEq(route.routingdata(), {'d_depot': u'0621', 'serviceinfo': u'', 'country': 'AT', 'd_sort': u'64', 'o_sort': u'62', 'service_text': u'D'}) def test_difficult_service(self): route = self.router.route(Destination('AT', '4240', 'Freistadt Österreich')) self.assertDicEq(route.routingdata(), {'d_depot': u'0634', 'serviceinfo': '', 'country': u'AT', 'd_sort': u'22', 'o_sort': u'62', 'service_text': u'D'}) route = self.router.route(Destination('AT', '5101', 'Bergheim bei Salzburg')) self.assertDicEq(route.routingdata(), {'d_depot': u'0625', 'serviceinfo': u'', 'country': 'AT', 'd_sort': u'2509', 'o_sort': u'62', 'service_text': u'D'}) route = self.router.route(Destination('AT', '8230', 'Hartberg')) self.assertDicEq(route.routingdata(), {'d_depot': u'0628', 'serviceinfo': u'', 'country': 'AT', 'd_sort': u'1290', 'o_sort': u'62', 'service_text': u'D'}) route = self.router.route(Destination('AT', '8045', 'Graz/<96>sterreich')) self.assertDicEq(route.routingdata(), {'d_depot': u'0628', 'serviceinfo': u'', 'country': 'AT', 'd_sort': u'2840', 'o_sort': u'62', 'service_text': u'D'}) def test_postcode_with_country(self): route = self.router.route(Destination(postcode='FR-66400', country='FR')) self.assertDicEq(route.routingdata(), {'d_depot': u'0470', 'serviceinfo': '', 'country': u'FR', 'd_sort': u'U50', 'o_sort': u'16', 'service_text': u'D'}) route = self.router.route(Destination(postcode='FR 66400', country='FR')) self.assertDicEq(route.routingdata(), {'d_depot': u'0470', 'serviceinfo': '', 'country': u'FR', 'd_sort': u'U50', 'o_sort': u'16', 'service_text': u'D'}) route = self.router.route(Destination(postcode='FR66400', country='FR')) self.assertDicEq(route.routingdata(), {'d_depot': '0470', 'serviceinfo': '', 'country': 'FR', 'd_sort': 'U50', 'o_sort': '16', 'service_text': 'D'}) route = self.router.route(Destination(postcode='F-66400', country='FR')) self.assertDicEq(route.routingdata(), {'d_depot': '0470', 'serviceinfo': '', 'country': 'FR', 'd_sort': 'U50', 'o_sort': '16', 'service_text': 'D'}) def test_postcode_spaces(self): route = self.router.route(Destination(postcode='42 477')) self.assertDicEq(route.routingdata(), {'o_sort': '42', 'serviceinfo': '', 'country': 'DE', 'd_sort': '65', 'd_depot': '0142', 'service_text': 'D'}) route = self.router.route(Destination(postcode=' 42477')) self.assertDicEq(route.routingdata(), {'d_depot': u'0142', 'serviceinfo': '', 'country': u'DE', 'd_sort': u'65', 'o_sort': u'42', 'service_text': u'D'}) route = self.router.route(Destination(postcode=' 42477 ')) self.assertDicEq(route.routingdata(), {'d_depot': '0142', 'serviceinfo': '', 'country': 'DE', 'd_sort': '65', 'o_sort': '42', 'service_text': 'D'}) # real live sample route = self.router.route(Destination('GB', 'GU148HN', 'Hampshire')) self.assertDicEq(route.routingdata(), {'d_depot': '1550', 'serviceinfo': '', 'country': 'GB', 'd_sort': '', 'o_sort': '52', 'service_text': 'D'}) route = self.router.route(Destination('GB', 'GU 14 8HN', 'Hampshire')) self.assertDicEq(route.routingdata(), {'d_depot': '1550', 'serviceinfo': '', 'country': 'GB', 'd_sort': '', 'o_sort': '52', 'service_text': 'D'}) def test_problematic_routes(self): # Lichtenstein is problematic because usually it is routed trough Swizerland. route = self.router.route(Destination('LI', '8440')) self.assertDicEq(route.routingdata(), {'d_depot': u'0617', 'serviceinfo': '', 'country': u'CH', 'd_sort': u'', 'o_sort': u'78', 'service_text': u'D'}) route = self.router.route(Destination('LI', '8440', 'Steinfort')) self.assertDicEq(route.routingdata(), {'d_depot': u'0617', 'serviceinfo': '', 'country': u'CH', 'd_sort': u'', 'o_sort': u'78', 'service_text': u'D'}) def test_international(self): # AR | 1426 | Buenos Aire self.assertDicEq(get_route('AR', '1426').routingdata(), {'d_depot': u'0920', 'serviceinfo': u'', 'country': u'AR', 'd_sort': u'', 'o_sort': u'16', 'service_text': u'D'}) # AZ | 1073 | Baku self.assertDicEq(get_route('AZ', '1073').routingdata(), {'d_depot': u'0918', 'serviceinfo': '', 'country': 'AZ', 'd_sort': u'CDG', 'o_sort': u'16', 'service_text': u'D'}) # BE | 3960 | Bree/Belgien self.assertDicEq(get_route('BE', '3960').routingdata(), {'d_depot': u'0532', 'serviceinfo': u'', 'country': u'BE', 'd_sort': u'A353', 'o_sort': u'52', 'service_text': u'D'}) # BG | 1766 | Sofia self.assertDicEq(get_route('BG', '1766').routingdata(), {'d_depot': u'1660', 'serviceinfo': '', 'country': u'BG', 'd_sort': u'', 'o_sort': u'62', 'service_text': u'D'}) # CH | 3601 | Thun/Schweiz self.assertDicEq(get_route('CH', '3601').routingdata(), {'d_depot': u'0612', 'serviceinfo': u'', 'country': u'CH', 'd_sort': u'', 'o_sort': u'78', 'service_text': u'D'}) # CZ | 16300 | Praha 6- Repy self.assertDicEq(get_route('CZ', '16300').routingdata(), {'d_depot': u'1391', 'serviceinfo': u'', 'country': u'CZ', 'd_sort': u'B854', 'o_sort': u'10', 'service_text': u'D'}) # CZ | 71000 | Ostrava self.assertDicEq(get_route('CZ', '71000').routingdata(), {'d_depot': u'1384', 'serviceinfo': u'', 'country': u'CZ', 'd_sort': u'412', 'o_sort': u'10', 'service_text': u'D'}) # DE | 04316 | Leipzig self.assertDicEq(get_route('DE', '04316').routingdata(), {'d_depot': u'0104', 'serviceinfo': '', 'country': u'DE', 'd_sort': u'2CPO', 'o_sort': u'10', 'service_text': u'D'}) # DE | 99974 | Mhlhausen / Thringen self.assertDicEq(get_route('DE', '99974').routingdata(), {'d_depot': u'0234', 'serviceinfo': u'', 'country': u'DE', 'd_sort': u'C015', 'o_sort': u'KK02', 'service_text': u'D'}) # DK | 4000 | Roskilde self.assertDicEq(get_route('DK', '4000').routingdata(), {'d_depot': u'0500', 'serviceinfo': u'', 'country': u'DK', 'd_sort': u'01', 'o_sort': u'20', 'service_text': u'D'}) # DK | 9500 | Hobro self.assertDicEq(get_route('DK', '9500').routingdata(), {'d_depot': u'0504', 'serviceinfo': u'', 'country': u'DK', 'd_sort': u'405', 'o_sort': u'20', 'service_text': u'D'}) # EE | 10621 | Tallinn self.assertDicEq(get_route('EE', '10621').routingdata(), {'d_depot': u'0560', 'serviceinfo': u'', 'country': u'EE', 'd_sort': u'0005', 'o_sort': u'13', 'service_text': u'D'}) # ES | 08227 | Terrassa self.assertDicEq(get_route('ES', '08227').routingdata(), {'d_depot': u'0708', 'serviceinfo': u'', 'country': u'ES', 'd_sort': u'01', 'o_sort': u'16', 'service_text': u'D'}) # FI | 94700 | Kemi self.assertDicEq(get_route('FI', '94700').routingdata(), {'d_depot': u'1614', 'serviceinfo': u'', 'country': u'FI', 'd_sort': u'510', 'o_sort': u'15', 'service_text': u'D'}) # FR | 91044 | EVRY-LISSES self.assertDicEq(get_route('FR', '91044').routingdata(), {'d_depot': u'0408', 'serviceinfo': '', 'country': u'FR', 'd_sort': u'S61', 'o_sort': u'50', 'service_text': u'D'}) # GB | BT387AR | Carrickfergus self.assertDicEq(get_route('GB', 'BT387AR').routingdata(), {'d_depot': u'1598', 'serviceinfo': u'', 'country': u'GB', 'd_sort': u'', 'o_sort': u'52', 'service_text': u'D'}) # GB | CB13SW | Cambridge self.assertDicEq(get_route('GB', 'CB13SW').routingdata(), {'d_depot': u'1550', 'serviceinfo': u'', 'country': u'GB', 'd_sort': u'', 'o_sort': u'52', 'service_text': u'D'}) # GB | G43 2DX | Glasgow self.assertDicEq(get_route('GB', 'G43 2DX').routingdata(), {'d_depot': u'1550', 'serviceinfo': u'', 'country': u'GB', 'd_sort': u'', 'o_sort': u'52', 'service_text': u'D'}) # GB | G432DX | Glasgow self.assertDicEq(get_route('GB', 'G432DX').routingdata(), {'d_depot': u'1550', 'serviceinfo': u'', 'country': u'GB', 'd_sort': u'', 'o_sort': u'52', 'service_text': u'D'}) # GB | N41NR | London self.assertDicEq(get_route('GB', 'N41NR').routingdata(), {'d_depot': u'1550', 'serviceinfo': u'', 'country': u'GB', 'd_sort': u'', 'o_sort': u'52', 'service_text': u'D'}) # GR | 17341 | Athens self.assertDicEq(get_route('GR', '17341').routingdata(), {'d_depot': u'1251', 'serviceinfo': u'', 'country': u'GR', 'd_sort': u'', 'o_sort': u'62', 'service_text': u'D'}) # GR | 64200 | Chrisoupoli-KAVALA self.assertDicEq(get_route('GR', '64200').routingdata(), {'d_depot': u'1251', 'serviceinfo': u'', 'country': u'GR', 'd_sort': u'', 'o_sort': u'62', 'service_text': u'D'}) # HR | 10000 | Zagreb self.assertDicEq(get_route('HR', '10000').routingdata(), {'d_depot': u'1750', 'serviceinfo': u'', 'country': u'HR', 'd_sort': u'SVI', 'o_sort': u'62', 'service_text': u'D'}) # HR | 44000 | Sisak self.assertDicEq(get_route('HR', '44000').routingdata(), {'d_depot': u'1750', 'serviceinfo': u'', 'country': u'HR', 'd_sort': u'020', 'o_sort': u'62', 'service_text': u'D'}) # HU | 1121 | Budapest self.assertDicEq(get_route('HU', '1121').routingdata(), {'d_depot': u'1640', 'serviceinfo': u'', 'country': u'HU', 'd_sort': u'027', 'o_sort': u'62', 'service_text': u'D'}) # HU | 9400 | Sopron self.assertDicEq(get_route('HU', '9400').routingdata(), {'d_depot': u'1657', 'serviceinfo': u'', 'country': u'HU', 'd_sort': u'684', 'o_sort': u'62', 'service_text': u'D'}) # IT | 34100 | Trieste / Italien self.assertDicEq(get_route('IT', '34100').routingdata(), {'d_depot': u'0835', 'serviceinfo': u'', 'country': u'IT', 'd_sort': u'01', 'o_sort': u'16', 'service_text': u'D'}) # LI | 09494 | Schaan self.assertDicEq(get_route('LI', '09494').routingdata(), {'d_depot': u'0617', 'serviceinfo': u'', 'country': u'CH', 'd_sort': u'', 'o_sort': u'78', 'service_text': u'D'}) # LI | 9999 | Wemperhardt self.assertDicEq(get_route('LI', '9999').routingdata(), {'d_depot': u'0617', 'serviceinfo': u'', 'country': u'CH', 'd_sort': u'', 'o_sort': u'78', 'service_text': u'D'}) # LI | CH-9491 | Ruggell self.assertDicEq(get_route('LI', 'CH-9491').routingdata(), {'d_depot': u'0617', 'serviceinfo': u'', 'country': u'CH', 'd_sort': u'', 'o_sort': u'78', 'service_text': u'D'}) # LI | 9491 | Ruggell self.assertDicEq(get_route('LI', '9491').routingdata(), {'d_depot': u'0617', 'serviceinfo': u'', 'country': u'CH', 'd_sort': u'', 'o_sort': u'78', 'service_text': u'D'}) # LT | 3031 | Kaunas self.assertDicEq(get_route('LT', '3031').routingdata(), {'d_depot': u'0594', 'serviceinfo': '', 'country': u'LT', 'd_sort': u'732', 'o_sort': u'13', 'service_text': u'D'}) # LV | 1039 | Riga self.assertDicEq(get_route('LV', '1039').routingdata(), {'d_depot': u'0575', 'serviceinfo': u'', 'country': u'LV', 'd_sort': u'R39', 'o_sort': u'13', 'service_text': u'D'}) # NL | 7443TC | Nijverdal self.assertDicEq(get_route('NL', '7443TC').routingdata(), {'d_depot': u'0512', 'serviceinfo': '', 'country': u'NL', 'd_sort': u'B535', 'o_sort': u'52', 'service_text': u'D'}) # NL | 9405 JB | Assen self.assertDicEq(get_route('NL', '9405 JB').routingdata(), {'d_depot': u'0513', 'serviceinfo': u'', 'country': u'NL', 'd_sort': u'B241', 'o_sort': u'52', 'service_text': u'D'}) # NO | 6800 | Förde self.assertDicEq(get_route('NO', '6800').routingdata(), {'d_depot': u'0360', 'serviceinfo': u'', 'country': u'NO', 'd_sort': u'01', 'o_sort': u'15', 'service_text': u'D'}) # PL | 80516 | Gdansk self.assertDicEq(get_route('PL', '80516').routingdata(), {'d_depot': u'1306', 'serviceinfo': u'', 'country': u'PL', 'd_sort': u'', 'o_sort': u'13', 'service_text': u'D'}) # PL | 22300 | Krasnystaw self.assertDicEq(get_route('PL', '22300').routingdata(), {'d_depot': u'1300', 'serviceinfo': u'', 'country': u'PL', 'd_sort': u'', 'o_sort': u'13', 'service_text': u'D'}) # SE | 11358 | Stockholm self.assertDicEq(get_route('SE', '11358').routingdata(), {'d_depot': u'0312', 'serviceinfo': u'', 'country': u'SE', 'd_sort': u'01', 'o_sort': u'20', 'service_text': u'D'}) # SE | 75752 | Uppsala self.assertDicEq(get_route('SE', '75752').routingdata(), {'d_depot': u'0312', 'serviceinfo': u'', 'country': u'SE', 'd_sort': u'01', 'o_sort': u'20', 'service_text': u'D'}) # SI | 1225 | Lukovica self.assertDicEq(get_route('SI', '1225').routingdata(), {'d_depot': u'1696', 'serviceinfo': '', 'country': u'SI', 'd_sort': u'14', 'o_sort': u'62', 'service_text': u'D'}) # SK | 82105 | Bratislava self.assertDicEq(get_route('SK', '82105').routingdata(), {'d_depot': u'0660', 'serviceinfo': '', 'country': u'SK', 'd_sort': u'10', 'o_sort': u'10', 'service_text': u'D'}) def test_incorrectCountry(self): self.assertRaises(CountryError, get_route, 'URG', '42477') def test_incorrectLocation(self): self.assertRaises(TranslationError, get_route, 'DE', None) def test_incorrectService(self): self.assertRaises(ServiceError, get_route, 'DE', '0001') def test_select_routes(self): self.router.conditions = ['1=1'] rows = self.router.select_routes('DestinationCountry=?', ('UZ', )) self.assert_(len(rows) > 0) def test_cache(self): self.assertDicEq(vars(get_route('LI', '8440')), vars(get_route_without_cache('LI', '8440'))) class HighLevelTest(TestCase): def test_get_route(self): self.assertDicEq(vars(get_route('DE', '42897')), {'service_mark': u'', 'o_sort': u'42', 'serviceinfo': u'', 'barcode_id': u'37', 'grouping_priority': u'', 'country': u'DE', 'countrynum': u'276', 'routingtable_version': u'20110905', 'iata_code': u'', 'd_sort': u'15', 'postcode': u'42897', 'd_depot': u'0142', 'service_text': u'D'}) self.assertDicEq(vars(get_route('DE', '42897', 'Remscheid')), {'service_mark': u'', 'o_sort': u'42', 'serviceinfo': u'', 'barcode_id': u'37', 'grouping_priority': u'', 'country': u'DE', 'countrynum': u'276', 'routingtable_version': u'20110905', 'iata_code': u'', 'd_sort': u'15', 'postcode': u'42897', 'd_depot': u'0142', 'service_text': u'D'}) self.assertDicEq(vars(get_route('DE', '42897', 'Remscheid', '101')), {'service_mark': u'', 'o_sort': u'42', 'serviceinfo': u'', 'barcode_id': u'37', 'grouping_priority': u'', 'country': u'DE', 'countrynum': u'276', 'routingtable_version': u'20110905', 'iata_code': u'', 'd_sort': u'15', 'postcode': u'42897', 'd_depot': u'0142', 'service_text': u'D'}) self.assertDicEq(vars(get_route('LI', '8440')), {'service_mark': u'', 'o_sort': u'78', 'serviceinfo': u'', 'barcode_id': u'37', 'grouping_priority': u'', 'country': u'CH', 'countrynum': u'756', 'routingtable_version': u'20110905', 'iata_code': u'', 'd_sort': u'', 'postcode': u'8440', 'd_depot': u'0617', 'service_text': u'D'}) self.assertDicEq(vars(get_route(u'LI', '8440')), {'service_mark': u'', 'o_sort': u'78', 'serviceinfo': u'', 'barcode_id': u'37', 'grouping_priority': u'', 'country': u'CH', 'countrynum': u'756', 'routingtable_version': u'20110905', 'iata_code': u'', 'd_sort': u'', 'postcode': u'8440', 'd_depot': u'0617', 'service_text': u'D'}) self.assertDicEq(vars(get_route(u'LI', u'8440')), {'service_mark': u'', 'o_sort': u'78', 'serviceinfo': u'', 'barcode_id': u'37', 'grouping_priority': u'', 'country': u'CH', 'countrynum': u'756', 'routingtable_version': u'20110905', 'iata_code': u'', 'd_sort': u'', 'postcode': u'8440', 'd_depot': u'0617', 'service_text': u'D'}) def test_cache(self): self.assertEqual(vars(get_route('LI', '8440')), vars(get_route_without_cache('LI', '8440'))) if __name__ == '__main__': start = time.time() router = Router(RouteData()) stamp = time.time() router.route(Destination('AT', '4240', 'Freistadt Österreich')).routingdata() end = time.time() # print ("took %.3fs to find a single route (including %.3fs initialisation overhead)" # % (end-start, stamp-start)) unittest.main() ================================================ FILE: pyshipping/carriers/dpd/georoutetables/COUNTRY ================================================ #Filename: COUNTRY #Version: 20140505 #Expiration: 20140831 #Hash: 01f9fd1927280385e2afd8287ca19c8528cb4ad2 #Reference: http://extranet.dpd.de/georoute/references_dpd_20140505.txt #Fields: ISO-NumCountryCode|ISO-Alpha2CountryCode|ISO-Alpha3CountryCode|DestinationLanguages|FlagPostCodeNo| #Key: ISO-NumCountryCode| 004|AF|AFG|EN|0| 008|AL|ALB|SQ|0| 010|AQ|ATA|EN|1| 012|DZ|DZA|FR|0| 016|AS|ASM|EN|0| 020|AD|AND|FR,ES,CA|0| 024|AO|AGO|PT|1| 028|AG|ATG|EN|1| 031|AZ|AZE|AZ|0| 032|AR|ARG|ES|0| 036|AU|AUS|EN|0| 040|AT|AUT|DE|0| 044|BS|BHS|EN|1| 048|BH|BHR|AR|0| 050|BD|BGD|BN,EN|0| 051|AM|ARM|HY|0| 052|BB|BRB|EN|1| 056|BE|BEL|FR,NL,DE|0| 060|BM|BMU|EN|0| 064|BT|BTN|DZ|0| 068|BO|BOL|ES,AY,QU|1| 070|BA|BIH|BS,HR,SR|0| 072|BW|BWA|EN|0| 074|BV|BVT|NO,EN|1| 076|BR|BRA|PT|0| 084|BZ|BLZ|EN|0| 086|IO|IOT|EN|0| 090|SB|SLB|EN|1| 092|VG|VGB|EN|0| 096|BN|BRN|MS,EN|0| 100|BG|BGR|BG|0| 104|MM|MMR|MY|0| 108|BI|BDI|FR,RN|1| 112|BY|BLR|BE,RU|0| 116|KH|KHM|KM|0| 120|CM|CMR|FR,EN|0| 124|CA|CAN|EN,FR|0| 132|CV|CPV|PT|0| 136|KY|CYM|EN|0| 140|CF|CAF|FR,SG|0| 144|LK|LKA|SI,TA,EN|0| 148|TD|TCD|FR,AR|1| 152|CL|CHL|ES|0| 156|CN|CHN|ZH|0| 158|TW|TWN|EN|0| 162|CX|CXR|EN|1| 166|CC|CCK|EN|0| 170|CO|COL|ES|1| 174|KM|COM|FR,AR|0| 175|YT|MYT|FR,MG,SW|0| 178|CG|COG|FR,LN|0| 180|CD|COD|FR|0| 184|CK|COK|EN|0| 188|CR|CRI|ES|0| 191|HR|HRV|HR|0| 192|CU|CUB|ES|0| 196|CY|CYP|EL,TR|0| 203|CZ|CZE|CS|0| 204|BJ|BEN|FR|0| 208|DK|DNK|DA|0| 212|DM|DMA|EN|0| 214|DO|DOM|ES|0| 218|EC|ECU|ES,QU|0| 222|SV|SLV|ES|1| 226|GQ|GNQ|ES,FR|1| 231|ET|ETH|AM|0| 232|ER|ERI|AR,TI,EN|0| 233|EE|EST|ET|0| 234|FO|FRO|FO|0| 238|FK|FLK|EN|0| 239|GS|SGS|EN|0| 242|FJ|FJI|EN|0| 246|FI|FIN|FI,SV|0| 248|AX|ALA|SV|0| 250|FR|FRA|FR|0| 254|GF|GUF|FR,EN|0| 258|PF|PYF|FR,TY|0| 260|TF|ATF|FR,EN|1| 262|DJ|DJI|FR,AR,AA|1| 266|GA|GAB|FR|0| 268|GE|GEO|KA|0| 270|GM|GMB|EN|1| 275|PS|PSE|AR,EN|0| 276|DE|DEU|DE|0| 288|GH|GHA|EN|1| 292|GI|GIB|ES,EN|1| 296|KI|KIR|EN|1| 300|GR|GRC|EL,EN|0| 304|GL|GRL|DA|0| 308|GD|GRD|EN|1| 312|GP|GLP|FR|0| 316|GU|GUM|EN|0| 320|GT|GTM|ES|0| 324|GN|GIN|FR|0| 328|GY|GUY|EN|0| 332|HT|HTI|FR,HT|0| 334|HM|HMD|EN|1| 336|VA|VAT|IT|0| 340|HN|HND|ES|0| 344|HK|HKG|EN,ZH|1| 348|HU|HUN|HU,EN|0| 352|IS|ISL|IS,EN|0| 356|IN|IND|EN,HI|0| 360|ID|IDN|ID|0| 364|IR|IRN|FA|0| 368|IQ|IRQ|AR,KU|0| 372|IE|IRL|EN,GA|0| 376|IL|ISR|AR,HE|0| 380|IT|ITA|IT|0| 384|CI|CIV|FR|0| 388|JM|JAM|EN|1| 392|JP|JPN|JA,EN|0| 398|KZ|KAZ|KK,RU|0| 400|JO|JOR|AR|0| 404|KE|KEN|EN,SW|0| 408|KP|PRK|KO|1| 410|KR|KOR|KO|0| 414|KW|KWT|AR|0| 417|KG|KGZ|UZ,KY,RU|0| 418|LA|LAO|LO|0| 422|LB|LBN|AR,EN,FR|1| 426|LS|LSO|EN|0| 428|LV|LVA|LV|0| 430|LR|LBR|EN|0| 434|LY|LBY|AR|0| 438|LI|LIE|DE|0| 440|LT|LTU|LT|0| 442|LU|LUX|LB,FR,DE|0| 446|MO|MAC|PT,ZH|1| 450|MG|MDG|MG,FR,EN|0| 454|MW|MWI|EN|1| 458|MY|MYS|MS|0| 462|MV|MDV|DV|0| 466|ML|MLI|FR|1| 470|MT|MLT|MT,EN|0| 474|MQ|MTQ|FR|0| 478|MR|MRT|AR|1| 480|MU|MUS|EN,FR|0| 484|MX|MEX|ES|0| 492|MC|MCO|FR|0| 496|MN|MNG|MN|0| 498|MD|MDA|MO|0| 499|ME|MNE|SR|0| 500|MS|MSR|EN|1| 504|MA|MAR|AR|0| 508|MZ|MOZ|PT|0| 512|OM|OMN|AR|0| 516|NA|NAM|AF,EN|1| 520|NR|NRU|NA|1| 524|NP|NPL|NE|0| 528|NL|NLD|NL|0| 530|AN|ANT|NL|1| 531|CW|CUW|EN,NL|1| 533|AW|ABW|EN|1| 534|SX|SXM|EN,NL|1| 535|BQ|BES|NL|1| 540|NC|NCL|FR|0| 548|VU|VUT|FR,EN|0| 554|NZ|NZL|EN|0| 558|NI|NIC|ES|0| 562|NE|NER|FR|0| 566|NG|NGA|EN|0| 570|NU|NIU|EN|1| 574|NF|NFK|EN|1| 578|NO|NOR|NO,NB,NN|0| 580|MP|MNP|EN|1| 581|UM|UMI|EN|1| 583|FM|FSM|EN|0| 584|MH|MHL|EN|1| 585|PW|PLW|EN|0| 586|PK|PAK|EN|0| 591|PA|PAN|ES|1| 598|PG|PNG|EN|0| 600|PY|PRY|ES,GN|0| 604|PE|PER|ES,QU,AY|0| 608|PH|PHL|EN|0| 612|PN|PCN|EN|0| 616|PL|POL|PL|0| 620|PT|PRT|PT|0| 624|GW|GNB|PT|0| 626|TL|TLS|PT,ID|1| 630|PR|PRI|ES,EN|0| 634|QA|QAT|AR|1| 638|RE|REU|FR|0| 642|RO|ROU|RO|0| 643|RU|RUS|RU|0| 646|RW|RWA|FR,EN,RW|1| 654|SH|SHN|EN|0| 659|KN|KNA|EN|1| 660|AI|AIA|EN|1| 662|LC|LCA|EN|1| 663|MF|MAF|FR|0| 666|PM|SPM|FR|0| 670|VC|VCT|EN|1| 674|SM|SMR|IT|0| 678|ST|STP|PT|1| 682|SA|SAU|AR|0| 686|SN|SEN|FR|0| 688|RS|SRB|SR|0| 690|SC|SYC|FR,EN|1| 694|SL|SLE|EN|1| 702|SG|SGP|EN,MS,TA|0| 703|SK|SVK|SK|0| 704|VN|VNM|VI|0| 705|SI|SVN|SL|0| 706|SO|SOM|SO,EN|0| 710|ZA|ZAF|AF,EN|0| 716|ZW|ZWE|EN|1| 724|ES|ESP|ES|0| 728|SS|SSD|EN|0| 729|SD|SDN|AR,EN|0| 732|EH|ESH|AR,ES,FR|0| 740|SR|SUR|EN,NL|1| 744|SJ|SJM|NO|0| 748|SZ|SWZ|EN,SS|0| 752|SE|SWE|SV|0| 756|CH|CHE|DE,FR,IT|0| 760|SY|SYR|AR|0| 762|TJ|TJK|TG|0| 764|TH|THA|TH|0| 768|TG|TGO|FR|1| 772|TK|TKL|EN|0| 776|TO|TON|EN,TO|1| 780|TT|TTO|EN|0| 784|AE|ARE|AR|1| 788|TN|TUN|AR|0| 792|TR|TUR|TR|0| 795|TM|TKM|TK|0| 796|TC|TCA|EN|0| 798|TV|TUV|EN|1| 800|UG|UGA|EN,SW|1| 804|UA|UKR|UK,RU|0| 807|MK|MKD|MK|0| 818|EG|EGY|AR|0| 826|GB|GBR|EN|0| 831|GG|GGY|EN|0| 832|JE|JEY|EN|0| 833|IM|IMN|EN|0| 834|TZ|TZA|EN,SW|1| 840|US|USA|EN|0| 850|VI|VIR|EN|0| 854|BF|BFA|FR|0| 858|UY|URY|ES|0| 860|UZ|UZB|UZ|0| 862|VE|VEN|ES|0| 876|WF|WLF|FR|0| 882|WS|WSM|SM|1| 887|YE|YEM|AR|0| 894|ZM|ZMB|EN|0| 991|IC|ISC|ES|0| 999|ZZ|ZZZ|EN|0| ================================================ FILE: pyshipping/carriers/dpd/georoutetables/LOCATION.DE ================================================ #Filename: LOCATION.DE #Version: 20140505 #Expiration: 20140831 #Hash: 6eedc807e450c4e8a76ee039ff22226d38b95f30 #Reference: http://extranet.dpd.de/georoute/references_dpd_20140505.txt #Fields: AreaName|CityName|ISO-Alpha2CountryCode|PostCode| #Key: AreaName|CityName| |Dublin|IE|1| |Irland ohne Dublin|IE|2| ================================================ FILE: pyshipping/carriers/dpd/georoutetables/LOCATION.EN ================================================ #Filename: LOCATION.EN #Version: 20140505 #Expiration: 20140831 #Hash: 6f7eff135d73b9bf60bc6d9c983313d8811fbcb2 #Reference: http://extranet.dpd.de/georoute/references_dpd_20140505.txt #Fields: AreaName|CityName|ISO-Alpha2CountryCode|PostCode| #Key: AreaName|CityName| |Dublin|IE|1| |Ireland excluding Dublin|IE|2| ================================================ FILE: pyshipping/carriers/dpd/georoutetables/LOCATION.FR ================================================ #Filename: LOCATION.FR #Version: 20140505 #Expiration: 20140831 #Hash: 71ba3db7983f8bcfa2a1b1cd327572ffcafaa331 #Reference: http://extranet.dpd.de/georoute/references_dpd_20140505.txt #Fields: AreaName|CityName|ISO-Alpha2CountryCode|PostCode| #Key: AreaName|CityName| |Dublin|IE|1| |Reste de l'Irlande (sauf Dublin)|IE|2| ================================================ FILE: pyshipping/carriers/dpd/georoutetables/SERVICE ================================================ #Filename: SERVICE #Version: 20140505 #Expiration: 20140831 #Hash: c8aa7333314e4057f7cf43d42ec66e9f89588345 #Reference: http://extranet.dpd.de/georoute/references_dpd_20140505.txt #Fields: ServiceCode|ServiceText|ServiceMark|ServiceElements| #Key: ServiceCode| 101|D||001| 102|D-HAZ||001,150| 103|D-6||001,130| 104|D-6-HAZ||001,130,150| 105|D-EXW||001,110| 106|D-EXW-HAZ||001,110,150| 107|D-6-EXW||001,110,130| 108|D-6-EXW-HAZ||001,110,130,150| 109|D-COD||001,100| 110|D-COD-HAZ||001,100,150| 111|D-6-COD||001,100,130| 112|D-6-COD-HAZ||001,100,130,150| 113|D-SWAP||001,120| 114|D-SWAP-HAZ||001,120,150| 115|D-6-SWAP||001,120,130| 116|D-6-SWAP-HAZ||001,120,130,150| 117|D||001,170| 118|D-BACK||001,121| 119|D-BACK-HAZ||001,121,150| 120|D+||001,180| 121|D-HAZ+||001,150,180| 122|D-6+||001,130,180| 123|D-6-HAZ+||001,130,150,180| 124|D-EXW+||001,110,180| 125|D-EXW-HAZ+||001,110,150,180| 126|D-6-EXW+||001,110,130,180| 127|D-6-EXW-HAZ+||001,110,130,150,180| 128|D-COD+||001,100,180| 129|D-COD-HAZ+||001,100,150,180| 130|D-6-COD+||001,100,130,180| 131|D-6-COD-HAZ+||001,100,130,150,180| 132|D-SWAP+||001,120,180| 133|D-SWAP-HAZ+||001,120,150,180| 134|D-6-SWAP+||001,120,130,180| 135|D-6-SWAP-HAZ+||001,120,130,150,180| 136|D|X|002| 137|D-6|X|002,130| 138|D-EXW|X|002,110| 139|D-6-EXW|X|002,110,130| 140|D-COD|X|002,100| 141|D-6-COD|X|002,100,130| 142|D-SWAP|X|002,120| 143|D-6-SWAP|X|002,120,130| 144|D|X|002,170| 145|D-BACK|X|002,121| 146|D+|X|002,180| 147|D-6+|X|002,130,180| 148|D-EXW+|X|002,110,180| 149|D-6-EXW+|X|002,110,130,180| 150|D-COD+|X|002,100,180| 151|D-6-COD+|X|002,100,130,180| 152|D-SWAP+|X|002,120,180| 153|D-6-SWAP+|X|002,120,130,180| 154|PARCELLetter|X|005| 155|PM2||010| 156|PM2-NO||010,160| 157|PM2-HAZ||010,150| 158|PM2-EXW||010,110| 159|PM2-EXW-NO||010,110,160| 160|PM2-EXW-HAZ||010,110,150| 161|PM2-COD||010,100| 162|PM2-COD-NO||010,100,160| 163|PM2-COD-HAZ||010,100,150| 164|PM2-SWAP||010,120| 165|PM2-SWAP-HAZ||010,120,150| 166|PM2-BACK||010,121| 167|PM2-BACK-HAZ||010,121,150| 168|PM2+||010,180| 169|PM2-NO+||010,160,180| 170|PM2-HAZ+||010,150,180| 171|PM2-EXW+||010,110,180| 172|PM2-EXW-NO+||010,110,160,180| 173|PM2-EXW-HAZ+||010,110,150,180| 174|PM2-COD+||010,100,180| 175|PM2-COD-NO+||010,100,160,180| 176|PM2-COD-HAZ+||010,100,150,180| 177|PM2-SWAP+||010,120,180| 178|PM2-SWAP-HAZ+||010,120,150,180| 179|AM1||022| 180|AM1-NO||022,160| 181|AM1-HAZ||022,150| 182|AM1-6||022,130| 183|AM1-6-NO||022,130,160| 184|AM1-6-HAZ||022,130,150| 185|AM1-EXW||022,110| 186|AM1-EXW-NO||022,110,160| 187|AM1-EXW-HAZ||022,110,150| 188|AM1-6-EXW||022,110,130| 189|AM1-6-EXW-NO||022,110,130,160| 190|AM1-6-EXW-HAZ||022,110,130,150| 191|AM1-COD||022,100| 192|AM1-COD-NO||022,100,160| 193|AM1-COD-HAZ||022,100,150| 194|AM1-6-COD||022,100,130| 195|AM1-6-COD-NO||022,100,130,160| 196|AM1-6-COD-HAZ||022,100,130,150| 197|AM1-SWAP||022,120| 198|AM1-SWAP-HAZ||022,120,150| 199|AM1-6-SWAP||022,120,130| 200|AM1-6-SWAP-HAZ||022,120,130,150| 201|AM1-BACK||022,121| 202|AM1-BACK-HAZ||022,121,150| 203|AM1+||022,180| 204|AM1-NO+||022,160,180| 205|AM1-HAZ+||022,150,180| 206|AM1-6+||022,130,180| 207|AM1-6-NO+||022,130,160,180| 208|AM1-6-HAZ+||022,130,150,180| 209|AM1-EXW+||022,110,180| 210|AM1-EXW-NO+||022,110,160,180| 211|AM1-EXW-HAZ+||022,110,150,180| 212|AM1-6-EXW+||022,110,130,180| 213|AM1-6-EXW-NO+||022,110,130,160,180| 214|AM1-6-EXW-HAZ+||022,110,130,150,180| 215|AM1-COD+||022,100,180| 216|AM1-COD-NO+||022,100,160,180| 217|AM1-COD-HAZ+||022,100,150,180| 218|AM1-6-COD+||022,100,130,180| 219|AM1-6-COD-NO+||022,100,130,160,180| 220|AM1-6-COD-HAZ+||022,100,130,150,180| 221|AM1-SWAP+||022,120,180| 222|AM1-SWAP-HAZ+||022,120,150,180| 223|AM1-6-SWAP+||022,120,130,180| 224|AM1-6-SWAP-HAZ+||022,120,130,150,180| 225|AM2||023| 226|AM2-NO||023,160| 227|AM2-HAZ||023,150| 228|AM2-6||023,130| 229|AM2-6-NO||023,130,160| 230|AM2-6-HAZ||023,130,150| 231|AM2-EXW||023,110| 232|AM2-EXW-NO||023,110,160| 233|AM2-EXW-HAZ||023,110,150| 234|AM2-6-EXW||023,110,130| 235|AM2-6-EXW-NO||023,110,130,160| 236|AM2-6-EXW-HAZ||023,110,130,150| 237|AM2-COD||023,100| 238|AM2-COD-NO||023,100,160| 239|AM2-COD-HAZ||023,100,150| 240|AM2-6-COD||023,100,130| 241|AM2-6-COD-NO||023,100,130,160| 242|AM2-6-COD-HAZ||023,100,130,150| 243|AM2-SWAP||023,120| 244|AM2-SWAP-HAZ||023,120,150| 245|AM2-6-SWAP||023,120,130| 246|AM2-6-SWAP-HAZ||023,120,130,150| 247|AM2-BACK||023,121| 248|AM2-BACK-HAZ||023,121,150| 249|AM2+||023,180| 250|AM2-NO+||023,160,180| 251|AM2-HAZ+||023,150,180| 252|AM2-6+||023,130,180| 253|AM2-6-NO+||023,130,160,180| 254|AM2-6-HAZ+||023,130,150,180| 255|AM2-EXW+||023,110,180| 256|AM2-EXW-NO+||023,110,160,180| 257|AM2-EXW-HAZ+||023,110,150,180| 258|AM2-6-EXW+||023,110,130,180| 259|AM2-6-EXW-NO+||023,110,130,160,180| 260|AM2-6-EXW-6-HAZ+||023,110,130,150,180| 261|AM2-COD+||023,100,180| 262|AM2-COD-NO+||023,100,160,180| 263|AM2-COD-HAZ+||023,100,150,180| 264|AM2-6-COD+||023,100,130,180| 265|AM2-6-COD-NO+||023,100,130,160,180| 266|AM2-6-COD-HAZ+||023,100,130,150,180| 267|AM2-SWAP+||023,120,180| 268|AM2-SWAP-HAZ+||023,120,150,180| 269|AM2-6-SWAP+||023,120,130,180| 270|AM2-6-SWAP-HAZ+||023,120,130,150,180| 271|SD||040| 272|SD-HAZ||040,150| 273|SD-EXW||040,110| 274|SD-EXW-HAZ||040,110,150| 275|SD-COD||040,100| 276|SD-COD-HAZ||040,100,150| 277|SD-SWAP||040,120| 278|SD-SWAP-HAZ||040,120,150| 279|SD-BACK||040,121| 280|SD-BACK-HAZ||040,121,150| 281|SD+||040,180| 282|SD-HAZ+||040,150,180| 283|SD-EXW+||040,110,180| 284|SD-EXW-HAZ+||040,110,150,180| 285|SD-COD+||040,100,180| 286|SD-COD-HAZ+||040,100,150,180| 287|SD-SWAP+||040,120,180| 288|SD-SWAP-HAZ+||040,120,150,180| 289|STANDARD BAG||060| 290|MAIL BAG||005,060| 291|IP||080| 292|POD BOX||081| 293|EXPRESS BAG||020,060| 294|MAIL||006,060| 298|RETURN SENDER||070,071| 299|RETURN|E|020,070| 300|RETURN||070| 301|RETURN-HAZ||070,150| 302|IE2|E|030| 303|IE2-MPS|E|030,140| 304|IE2-6|E|030,130| 305|IE2-6-MPS|E|030,130,140| 306|IE2-SWAP|E|030,120| 307|IE2-SWAP-MPS|E|030,120,140| 308|IE2-6-SWAP|E|030,120,130| 309|IE2-6-SWAP-MPS|E|030,120,130,140| 310|IE2-COD|E|030,100| 311|IE2-COD-MPS|E|030,100,140| 312|IE2-6-COD|E|030,100,130| 313|IE2-6-COD-MPS|E|030,100,130,140| 314|IE1|E|031| 325|D||001,012| 326|D|X|002,012| 327|D-B2C||001,013| 328|D-B2C|X|002,013| 329|D-COD-B2C||001,013,100| 330|D-COD-B2C|X|002,013,100| 331|D-TYRE UNP||001,014| 332|RETURN||072| 333|D-B2C+||001,013,180| 334|D-B2C+|X|002,013,180| 335|D-COD-B2C+||001,013,100,180| 336|D-COD-B2C+|X|002,013,100,180| 337|D-B2C-PSD||001,013,200| 338|D-B2C-PSD|X|002,013,200| 340|DPD MAX||090| 341|D-B2C-COD-PSD||001,013,100,200| 342|D-B2C-COD-PSD|X|002,013,100,200| 350|AM0||021| 351|AM0-EXW||021,110| 352|AM0-COD||021,100| 353|AM0-SWAP||021,120| 354|AM0-BACK||021,121| 355|AM0+||021,180| 356|AM0-EXW+||021,110,180| 357|AM0-COD+||021,100,180| 358|AM0-SWAP+||021,120,180| 359|D||001,190| 360|D|X|002,190| 361|D||001,110,190| 362|D|X|002,110,190| 363|D||001,210| 364|D|X|002,210| 365|D-TYRE||001,240| 366|D-TYRE-B2C||001,240,013| 367|D-TYRE-COD||001,240,100| 800|EXP||020| 801|EXP-COD||020,100| 802|EXP-EXW||020,110| 803|PS||610| 804|PS-COD||610,100| 805|PS-EXP||610,020| 806|D-SWAP-B2C+||001,013,120,180| 807|D-6-SWAP-B2C+||001,013,120,130,180| 808|D-SWAP-COD-B2C+||001,013,100,120,180| 809|D6-SWAP-COD-B2C+||001,013,100,120,130,180| 810|AM1||022| 811|AM2||023| 812|PM2||010| 813|TFR||600| 814|AM1-6||022,130| 815|AM2-6||023,130| 816|FIX||601| 817|PRIVAT||602| 818|D-SWAP-COD+||001,100,120,180| 819|D-6-SWAP-COD+||001,100,120,130,180| 820|D-6-B2C+||001,013,130,180| 821|D-6-B2C+|X|002,013,130,180| 822|D-6-COD-B2C+||001,013,100,130,180| 823|D-6-COD-B2C+|X|002,013,100,130,180| 824|D-ECO||001,250| 825|DPD MAX||090| 826|DPD MAX - COD||090,100| 827|D-SWAP-B2C||001,013,120| 828|D-SWAP-B2C|X|002,013,120| 829|D-6-B2C||001,013,130| 830|D-6-B2C|X|002,013,130| 831|D-6-COD-B2C||001,013,100,130| 832|D-6-COD-B2C|X|002,013,100,130| 833|D-6-SWAP-B2C||001,013,120,130| 834|D-6-SWAP-B2C|X|002,013,120,130| 835|D-SWAP-COD-B2C||001,013,100,120| 836|D-SWAP-COD-B2C|X|002,013,100,120| 837|D-6-SWAP-COD-B2C||001,013,100,120,130| 838|D-6-SWAP-COD-B2C|X|002,013,100,120,130| 839|D-EVE||001,220| 840|D-EVE|X|002,220| 841|D-COD-EVE||001,100,220| 842|D-COD-EVE|X|002,100,220| 843|SPAL||230| 844|D-B2C-PRIV||001,013,620| 845|D-B2C-PRIV|X|002,013,620| 846|D-B2C-HOME||001,013,621| 847|D-B2C-HOME|X|002,013,621| 848|D-SWAP-COD||001,100,120| 849|D-ECO-COD||001,250,100| 850|AM2-ECH-SHOP-NO||023,529,511,160| ================================================ FILE: pyshipping/carriers/dpd/georoutetables/SERVICEINFO.CS ================================================ #Filename: SERVICEINFO.CS #Version: 20140505 #Expiration: 20140831 #Hash: 755fd81761d0474d68f00ff70ce4516fa8fef173 #Reference: http://extranet.dpd.de/georoute/references_dpd_20140505.txt #Fields: ServiceCode|ServiceFieldInfo| #Key: ServiceCode| 102|nebezpecne zbozi / hazardous goods| 103|sobota /saturday| 106|ex works nebezpecne zbozi / ex works hazardous goods| 109|dobirka / C.O.D.| 110|dobirka nebezpecne zbozi / C.O.D. hazardous goods| 113|vymena / exchange| 117|Collection-upon-Delivery| 121|nebezpecne zbozi / hazardous goods| 124|ex works| 125|ex works nebezpecne zbozi / ex works hazardous goods| 128|dobirka / C.O.D.| 129|dobirka nebezpecne zbozi / C.O.D. hazardous goods| 132|vymena / exchange| 137|sobota / saturday| 138|ex works| 140|dobirka / C.O.D.| 142|vymena / exchange| 144|Collection-upon-Delivery| 148|ex works| 150|dobirka / C.O.D.| 152|vymena / exchange| 154|DPD PARCELLetter| 155|DPD 18:00 / DPD GUARANTEE| 158|DPD 18:00 ex works / DPD GUARANTEE ex works| 161|DPD 18:00 dobirka / DPD GUARANTEE C.O.D.| 164|DPD 18:00 vymena / DPD GUARANTEE exchange| 168|DPD 18:00 / DPD GUARANTEE| 171|DPD 18:00 ex works / DPD GUARANTEE ex works| 174|DPD 18:00 dobirka / DPD GUARANTEE C.O.D.| 179|DPD 10:00| 185|DPD 10:00 ex works| 191|DPD 10:00 dobirka / C.O.D.| 197|DPD 10:00 vymena / exchange| 203|DPD 10:00| 209|DPD 10:00 ex works| 215|DPD 10:00 dobirka / C.O.D.| 225|DPD 12:00| 228|DPD 12:00 sobota / Saturday| 231|DPD 12:00 ex works| 234|DPD 12:00 sobota ex works / Saturday ex works| 237|DPD 12:00 dobirka / C.O.D.| 240|DPD 12:00 sobota dobirka / Saturday C.O.D.| 243|DPD 12:00 vymena / exchange| 249|DPD 12:00| 252|DPD 12:00 sobota / Saturday| 255|DPD 12:00 ex works| 258|DPD 12:00 sobota ex works / Saturday ex works| 261|DPD 12:00 dobirka / C.O.D.| 271|stejny den / same day| 327|PRIVATE ADDRESS / B2C| 328|PRIVATE ADDRESS / B2C| 329|PRIVATE ADDRESS / B2C - C.O.D.| 330|PRIVATE ADDRESS / B2C - C.O.D.| 333|SOUKROMA ADRESA / KONTAKTOVAT PRIJEMCE| 334|SOUKROMA ADRESA / KONTAKTOVAT PRIJEMCE| 335|SOUKROMA ADRESA DOBIRKA / KONTAKTOVAT PRIJEMCE| 336|SOUKROMA ADRESA DOBIRKA / KONTAKTOVAT PRIJEMCE| 352|DPD 8:30 dobirka / C.O.D.| 353|DPD 8:30 vymena / exchange| 357|DPD 8:30 dobirka / C.O.D.| 358|DPD 8:30 vymena / exchange| 839|VECERNI DORUCENI 17:00-20:00| 840|VECERNI DORUCENI 17:00-20:00| 841|VECERNI DORUCENI 17:00-20:00 DOBIRKA| 842|VECERNI DORUCENI 17:00-20:00 DOBIRKA| ================================================ FILE: pyshipping/carriers/dpd/georoutetables/SERVICEINFO.CZ ================================================ #Filename: SERVICEINFO.CZ #Version: 20110905 #Expiration: 20120101 #Hash: de42bef2636b38cd7f371000f71df64f66cc0eda #Reference: http://extranet.dpd.de/georoute/references_dpd_20110905.txt #Fields: ServiceCode|ServiceFieldInfo| #Key: ServiceCode| 102|nebezpecne zbozi / hazardous goods| 103|sobota /saturday| 106|ex works nebezpecne zbozi / ex works hazardous goods| 109|dobirka / C.O.D.| 110|dobirka nebezpecne zbozi / C.O.D. hazardous goods| 113|vymena / exchange| 117|Collection-upon-Delivery| 121|nebezpecne zbozi / hazardous goods| 124|ex works| 125|ex works nebezpecne zbozi / ex works hazardous goods| 128|dobirka / C.O.D.| 129|dobirka nebezpecne zbozi / C.O.D. hazardous goods| 132|vymena / exchange| 137|sobota / saturday| 138|ex works| 140|dobirka / C.O.D.| 142|vymena / exchange| 144|Collection-upon-Delivery| 148|ex works| 150|dobirka / C.O.D.| 152|vymena / exchange| 154|DPD PARCELLetter| 155|DPD 18:00 / DPD GUARANTEE| 158|DPD 18:00 ex works / DPD GUARANTEE ex works| 161|DPD 18:00 dobirka / DPD GUARANTEE C.O.D.| 164|DPD 18:00 vymena / DPD GUARANTEE exchange| 168|DPD 18:00 / DPD GUARANTEE| 171|DPD 18:00 ex works / DPD GUARANTEE ex works| 174|DPD 18:00 dobirka / DPD GUARANTEE C.O.D.| 179|DPD 10:00| 185|DPD 10:00 ex works| 191|DPD 10:00 dobirka / C.O.D.| 197|DPD 10:00 vymena / exchange| 203|DPD 10:00| 209|DPD 10:00 ex works| 215|DPD 10:00 dobirka / C.O.D.| 225|DPD 12:00| 228|DPD 12:00 sobota / Saturday| 231|DPD 12:00 ex works| 234|DPD 12:00 sobota ex works / Saturday ex works| 237|DPD 12:00 dobirka / C.O.D.| 240|DPD 12:00 sobota dobirka / Saturday C.O.D.| 243|DPD 12:00 vymena / exchange| 249|DPD 12:00| 252|DPD 12:00 sobota / Saturday| 255|DPD 12:00 ex works| 258|DPD 12:00 sobota ex works / Saturday ex works| 261|DPD 12:00 dobirka / C.O.D.| 271|stejny den / same day| 327|SOUKROMA ADRESA / KONTAKTOVAT PRIJEMCE| 328|SOUKROMA ADRESA / KONTAKTOVAT PRIJEMCE| 329|SOUKROMA ADRESA DOBIRKA / KONTAKTOVAT PRIJEMCE| 330|SOUKROMA ADRESA DOBIRKA / KONTAKTOVAT PRIJEMCE| 333|SOUKROMA ADRESA / KONTAKTOVAT PRIJEMCE| 334|SOUKROMA ADRESA / KONTAKTOVAT PRIJEMCE| 335|SOUKROMA ADRESA DOBIRKA / KONTAKTOVAT PRIJEMCE| 336|SOUKROMA ADRESA DOBIRKA / KONTAKTOVAT PRIJEMCE| 352|DPD 8:30 dobirka / C.O.D.| 353|DPD 8:30 vymena / exchange| 357|DPD 8:30 dobirka / C.O.D.| 358|DPD 8:30 vymena / exchange| ================================================ FILE: pyshipping/carriers/dpd/georoutetables/SERVICEINFO.DE ================================================ #Filename: SERVICEINFO.DE #Version: 20140505 #Expiration: 20140831 #Hash: 8124d0f60be0fe5c28a7f0af019c975fc2bd3542 #Reference: http://extranet.dpd.de/georoute/references_dpd_20140505.txt #Fields: ServiceCode|ServiceFieldInfo| #Key: ServiceCode| 102|Gefahrgut / hazardous goods| 103|Samstag / saturday| 105|Unfrei / ex works| 106|Unfrei / ex works Gefahrgut / hazardous goods| 109|Nachnahme / C.O.D.| 110|Nachnahme / C.O.D. Gefahrgut / hazardous goods| 113|Austausch / exchange| 117|Mitnahme/Collection-upon-Delivery| 121|Gefahrgut / hazardous goods| 124|Unfrei / ex works| 125|Unfrei / ex works Gefahrgut / hazardous goods| 128|Nachnahme / C.O.D.| 129|Nachnahme / C.O.D. Gefahrgut / hazardous goods| 132|Austausch / exchange| 137|Samstag / saturday| 138|Unfrei / ex works| 140|Nachnahme / C.O.D.| 142|Austausch / exchange| 144|Mitnahme/Collection-upon-Delivery| 148|Unfrei / ex works| 150|Nachnahme / C.O.D.| 152|Austausch / exchange| 154|DPD PARCELLetter| 155|DPD 18:00 / DPD GUARANTEE| 158|DPD 18:00 / DPD GUARANTEE Unfrei / ex works| 161|DPD 18:00 / DPD GUARANTEE Nachnahme / C.O.D.| 164|DPD 18:00 / DPD GUARANTEE Austausch / exchange| 168|DPD 18:00 / DPD GUARANTEE| 171|DPD 18:00 / DPD GUARANTEE Unfrei / ex works| 174|DPD 18:00 / DPD GUARANTEE Nachnahme / C.O.D.| 179|DPD 10:00| 185|DPD 10:00 Unfrei / ex works| 191|DPD 10:00 Nachnahme / C.O.D.| 197|DPD 10:00 Austausch / exchange| 203|DPD 10:00| 209|DPD 10:00 Unfrei / ex works| 215|DPD 10:00 Nachnahme / C.O.D.| 225|DPD 12:00| 228|DPD 12:00 Samstag / saturday| 231|DPD 12:00 Unfrei / ex works| 234|DPD 12:00 Samstag / saturday Unfrei / ex works| 237|DPD 12:00 Nachnahme / C.O.D.| 240|DPD 12:00 Samstag / saturday Nachnahme / C.O.D.| 243|DPD 12:00 Austausch / exchange| 249|DPD 12:00| 252|DPD 12:00 Samstag / saturday| 255|DPD 12:00 Unfrei / ex works| 258|DPD 12:00 Samstag / saturday Unfrei / ex works| 261|DPD 12:00 Nachnahme / C.O.D.| 264|DPD 12:00 Samstag / saturday Nachnahme / C.O.D.| 270|DPD 12:00 Samstag Austausch Gefahrgut / Saturday Swap Hazardous Goods| 271|same day| 294|DPD Mail| 302|DPD EXPRESS| 325|EXPRESS ECONOMY| 326|EXPRESS ECONOMY| 329|Nachnahme / C.O.D.| 330|Nachnahme / C.O.D.| 335|Nachnahme / C.O.D.| 336|Nachnahme / C.O.D.| 337|DPD PaketShop Zustellung / Parcel Shop Delivery| 338|DPD PaketShop Zustellung / Parcel Shop Delivery| 341|DPD PaketShop Zustellung Nachnahme / Parcel Shop Delivery C.O.D.| 342|DPD PaketShop Zustellung Nachnahme / Parcel Shop Delivery C.O.D.| 350|DPD 8:30| 351|DPD 8:30 Unfrei / ex works| 352|DPD 8:30 Nachnahme / C.O.D.| 353|DPD 8:30 Austausch / exchange| 354|DPD 8:30| 355|DPD 8:30| 356|DPD 8:30 Unfrei / ex works| 357|DPD 8:30 Nachnahme / C.O.D.| 358|DPD 8:30 Austausch / exchange| 361|Unfrei / ex works| 362|Unfrei / ex works| 365|Reifen / Tyre| 366|Reifen / Tyre B2C| 367|Reifen / Tyre Nachnahme / C.O.D.| 810|Service Werktag 09:00 Uhr| 811|Service Werktag 12:00 Uhr| 812|Service Werktag 17:00 Uhr| 813|Service Werktag Zeitfenster| 814|Service Samstag 09:00 Uhr| 815|Service Samstag 12:00 Uhr| 816|Service Fixtermin| 817|Privatpaket| 820|Samstag / saturday| 821|Samstag / saturday| 822|Samstag / saturday Nachnahme / C.O.D.| 823|Samstag / saturday Nachnahme / C.O.D.| 827|Austausch / exchange| 828|Austausch / exchange| 829|Samstag / saturday| 830|Samstag / saturday| 831|Samstag / saturday Nachnahme / C.O.D.| 832|Samstag / saturday Nachnahme / C.O.D.| 833|Samstag / saturday Austausch / exchange| 834|Samstag / saturday Austausch / exchange| 835|Austausch / exchange Nachnahme / C.O.D.| 836|Austausch / exchange Nachnahme / C.O.D.| 837|Samstag / saturday Austausch / exchange Nachnahme / C.O.D.| 838|Samstag / saturday Austausch / exchange Nachnahme / C.O.D.| 839|Abendzustellung / Evening Delivery| 840|Abendzustellung / Evening Delivery| 841|Abendzustellung Nachnahme / Evening Delivery C.O.D.| 842|Abendzustellung Nachnahme / Evening Delivery C.O.D.| 844|Consumer Home| 845|Consumer Home| 846|Consumer Home & Shop| 847|Consumer Home & Shop| ================================================ FILE: pyshipping/carriers/dpd/georoutetables/SERVICEINFO.EE ================================================ #Filename: SERVICEINFO.EE #Version: 20110905 #Expiration: 20120101 #Hash: d8d55ca2e78b6aad2ecb2a4169dd58b108007c6e #Reference: http://extranet.dpd.de/georoute/references_dpd_20110905.txt #Fields: ServiceCode|ServiceFieldInfo| #Key: ServiceCode| 102|hazardous goods| 103|saturday| 105|ex works| 106|ex works / hazardous goods| 109|Lunapakk (COD)| 110|Lunapakk (COD) / hazardous goods| 113|exchange| 117|Collection-upon-Delivery| 121|hazardous goods| 124|ex works| 125|ex works / hazardous goods| 128|Lunapakk (COD)| 129|Lunapakk (COD) / hazardous goods| 132|exchange| 137|saturday| 138|ex works| 140|Lunapakk (COD)| 142|exchange| 144|Collection-upon-Delivery| 148|ex works| 150|Lunapakk (COD)| 152|exchange| 154|DPD PARCELLetter| 155|DPD GUARANTEE| 158|DPD GUARANTEE ex works| 161|DPD GUARANTEE Lunapakk (COD)| 164|DPD GUARANTEE exchange| 168|DPD GUARANTEE| 171|DPD GUARANTEE ex works| 174|DPD GUARANTEE Lunapakk (COD)| 179|DPD 10:00| 185|DPD 10:00 ex works| 191|DPD 10:00 Lunapakk (COD)| 197|DPD 10:00 exchange| 203|DPD 10:00| 209|DPD 10:00 ex works| 215|DPD 10:00 Lunapakk (COD)| 225|DPD 12:00| 228|DPD 12:00 saturday| 231|DPD 12:00 ex works| 234|DPD 12:00 saturday / ex works| 237|DPD 12:00 Lunapakk (COD)| 240|DPD 12:00 saturday / Lunapakk (COD)| 243|DPD 12:00 exchange| 249|DPD 12:00| 252|DPD 12:00 saturday| 255|DPD 12:00 ex works| 258|DPD 12:00 saturday / ex works| 261|DPD 12:00 Lunapakk (COD)| 264|DPD 12:00 saturday / Lunapakk (COD)| 271|same day| 294|DPD Mail| 302|DPD EXPRESS| 325|EXPRESS ECONOMY| 326|EXPRESS ECONOMY| 329|Lunapakk (COD)| 330|Lunapakk (COD)| 335|Lunapakk (COD)| 336|Lunapakk (COD)| 350|DPD 8:30| 351|DPD 8:30 ex works| 352|DPD 8:30 Lunapakk (COD)| 353|DPD 8:30 exchange| 354|DPD 8:30| 355|DPD 8:30| 356|DPD 8:30 ex works| 357|DPD 8:30 Lunapakk (COD)| 358|DPD 8:30 exchange| 820|saturday| 821|saturday| 822|saturday / Lunapakk (COD)| 823|saturday / Lunapakk (COD)| 827|exchange| 828|exchange| 829|saturday| 830|saturday| 831|saturday / Lunapakk (COD)| 832|saturday / Lunapakk (COD)| 833|saturday / exchange| 834|saturday / exchange| 835|exchange / Lunapakk (COD)| 836|exchange / Lunapakk (COD)| 837|saturday / exchange / Lunapakk (COD)| 838|saturday / exchange / Lunapakk (COD)| 840|1 day transit time| 841|2 days transit time| 842|3 days transit time| 843|4 days transit time| 844|5 days transit time| 845|1 day transit time / Lunapakk (COD)| 846|2 days transit time / Lunapakk (COD)| 847|3 days transit time / Lunapakk (COD)| 848|4 days transit time / Lunapakk (COD)| 849|5 days transit time / Lunapakk (COD)| ================================================ FILE: pyshipping/carriers/dpd/georoutetables/SERVICEINFO.EN ================================================ #Filename: SERVICEINFO.EN #Version: 20140505 #Expiration: 20140831 #Hash: 85037002df19b84563bcfdbc1153cd85ff629e9f #Reference: http://extranet.dpd.de/georoute/references_dpd_20140505.txt #Fields: ServiceCode|ServiceFieldInfo| #Key: ServiceCode| 102|hazardous goods| 103|saturday| 105|ex works| 106|ex works / hazardous goods| 109|C.O.D.| 110|C.O.D. / hazardous goods| 113|exchange| 117|Collection-upon-Delivery| 121|hazardous goods| 124|ex works| 125|ex works / hazardous goods| 128|C.O.D.| 129|C.O.D. / hazardous goods| 132|exchange| 137|saturday| 138|ex works| 140|C.O.D.| 142|exchange| 144|Collection-upon-Delivery| 148|ex works| 150|C.O.D.| 152|exchange| 154|DPD PARCELLetter| 155|DPD GUARANTEE| 158|DPD GUARANTEE ex works| 161|DPD GUARANTEE C.O.D.| 164|DPD GUARANTEE exchange| 168|DPD GUARANTEE| 171|DPD GUARANTEE ex works| 174|DPD GUARANTEE C.O.D.| 179|DPD 10:00| 185|DPD 10:00 ex works| 191|DPD 10:00 C.O.D.| 197|DPD 10:00 exchange| 203|DPD 10:00| 209|DPD 10:00 ex works| 215|DPD 10:00 C.O.D.| 225|DPD 12:00| 228|DPD 12:00 saturday| 231|DPD 12:00 ex works| 234|DPD 12:00 saturday / ex works| 237|DPD 12:00 C.O.D.| 240|DPD 12:00 saturday / C.O.D.| 243|DPD 12:00 exchange| 249|DPD 12:00| 252|DPD 12:00 saturday| 255|DPD 12:00 ex works| 258|DPD 12:00 saturday / ex works| 261|DPD 12:00 C.O.D.| 264|DPD 12:00 saturday / C.O.D.| 270|DPD 12:00 Saturday Swap Hazardous Goods| 271|same day| 294|DPD Mail| 302|DPD EXPRESS| 325|EXPRESS ECONOMY| 326|EXPRESS ECONOMY| 329|C.O.D.| 330|C.O.D.| 335|C.O.D.| 336|C.O.D.| 337|Parcel Shop Delivery| 338|Parcel Shop Delivery| 341|Parcel Shop Delivery C.O.D.| 342|Parcel Shop Delivery C.O.D.| 350|DPD 8:30| 351|DPD 8:30 ex works| 352|DPD 8:30 C.O.D.| 353|DPD 8:30 exchange| 354|DPD 8:30| 355|DPD 8:30| 356|DPD 8:30 ex works| 357|DPD 8:30 C.O.D.| 358|DPD 8:30 exchange| 361|ex works| 362|ex works| 365|Tyre| 366|Tyre B2C| 367|Tyre C.O.D.| 820|saturday| 821|saturday| 822|saturday / C.O.D.| 823|saturday / C.O.D.| 827|exchange| 828|exchange| 829|saturday| 830|saturday| 831|saturday / C.O.D.| 832|saturday / C.O.D.| 833|saturday / exchange| 834|saturday / exchange| 835|exchange / C.O.D.| 836|exchange / C.O.D.| 837|saturday / exchange / C.O.D.| 838|saturday / exchange / C.O.D.| 839|Evening Delivery| 840|Evening Delivery| 841|Evening Delivery COD| 842|Evening Delivery COD| 843|Special Pallet| 844|Consumer Home| 845|Consumer Home| 846|Consumer Home & Shop| 847|Consumer Home & Shop| 848|4 days transit time / C.O.D.| 849|5 days transit time / C.O.D.| ================================================ FILE: pyshipping/fortras/__init__.py ================================================ """This module contains tools for reading and writing Fortras messages. Fortras is a EDI standard for logistics related information somewhat common in Germany. See http://de.wikipedia.org/wiki/Fortras for further enlightenment.""" # You may consider this BSD licensed. from pyshipping.fortras.bordero import ship __all__ = [ship] ================================================ FILE: pyshipping/fortras/bordero.py ================================================ #!/usr/bin/env python # encoding: utf-8 """ bordero.py - implements BORD messages. BORD is similar to IFTMIN in EDIFACT. Think of it as an work order to a freight forwarder. This implementation is specific for usage between HUDORA GmbH and Mäuler but could be used as the basis for a more generic implementation. Created by Lars Ronge and Maximillian Dornseif on 2006-11-06. You may consider this BSD licensed. """ import os import time #def _transcode(data): # """Decode utf-8 to latin1 (which is used by fortras).""" # return data.decode('utf-8', 'replace').encode('latin-1', 'replace') def _clip(length, data): """Clip a string to a maximum length.""" # I wonder if this can't be done with clever formatstring usage. if not isinstance(data, str): data = data.decode('latin-1') if len(data) > length: return data[:length] return data # zwei führende Nullen weg !!! # übertragung: "übergepackt" # übertragung: Palettenformat 1.5, 1, 0.5 # überticragung: lieferfenster # DFü: Teil- Und Komplettladungen - verkehrsartschluessel : statt L (LKW) ein X (komplettladung) # Mäuler Telefonummer # Liste Auslandssendungen #doctext = """Bordero-Kopf-Satz""" #Afelder = [ # dict(length=18, startpos=0, endpos=19, name='borderonr', fieldclass=IntegerFieldZeropadded), # dict(length=8, startpos=19, endpos=27, name='datum', fieldclass=DateFieldReverse, # default=datetime.date.today()), # dict(length=1, startpos=27, endpos=28, name='versandweg', fieldclass=RightAdjustedField, # default='L', choices=('L', 'X')), # dict(length=2, startpos=28, endpos=30, name='fahrzeugnummer', fieldclass=IntegerFieldZeropadded), # dict(length=10, startpos=34, endpos=44, name='versenderid', fieldclass=RightAdjustedField, # default='L515'), # dict(length=13, startpos=44, endpos=57, name='frachtfuehrer', fieldclass=RightAdjustedField, # default='Maeuler'), # dict(length=9, startpos=53, endpos=63, name='plz', fieldclass=RightAdjustedField, # default='42897'), # dict(length=12, startpos=63, endpos=76, name='ort', fieldclass=RightAdjustedField, # default='Remscheid'), # dict(length=15, startpos=76, endpos=91, name='ladeeinheit1', fieldclass=RightAdjustedField), # dict(length=15, startpos=91, endpos=106, name='ladeeinheit1', fieldclass=RightAdjustedField), # dict(length=10, startpos=106, endpos=117, name='plombe1', fieldclass=RightAdjustedField), # dict(length=10, startpos=117, endpos=127, name='plombe1', fieldclass=RightAdjustedField), # dict(length=10, startpos=117, endpos=127, name='release', fieldclass=FixedField, default='6'), #] # Verkehrsartschlüssel (Satzart ‘A’, Position 031) # B = Bahn # E = Bordero für den eigenen Nahverkehr # H = VP an ZKP # K = Kombi # L = LKW # P = Packmittel-Clearing # W = Weiterleitungsbordero # X = Teilladungs-/Ladungspartie # Y = Bordero ZKP an EB ohne Weiterverladung # Z = ZKP an EP # C = Codis # N = Night Star Express # S = SystemPlus international # T = Teppichkurier #doctext="""Versender-Adress-Satz - Teil 1""" #Bfelder = [ # dict(length=35, startpos= 0 , endpos= 35, name='versender_name1', default='HUDORA GmbH'), # dict(length=35, startpos= 35, endpos= 70, name='versender_name2'), # dict(length=35, startpos= 70, endpos=105, name='versender_strasse', default='Jägerwald 13'), # dict(length= 3, startpos=105, endpos=107, name='versender_land', default='DE'), # dict(length= 9, startpos=107, endpos=116, name='versender_plz', default='42897'), # dict(length= 3, startpos=116, endpos=119, name='frei', fieldclass=FixedField, default=' '), # dict(length= 3, startpos=119, endpos=122, name='codis_abholstelle', fieldclass=FixedField, default=' '), # dict(length= 1, startpos=122, endpos=123, name='codis_laufkennzeichen', # fieldclass=FixedField, default=' '), #] # #doctext="""Versender-Adress-Satz - Teil 2""" #Cfelder = [ # dict(length=35, startpos= 0, endpos= 35, name='versenderort'), # dict(length= 3, startpos= 35, endpos= 38, name='versender_berechnungsland', fieldclass=FixedField, # default=' '), # dict(length= 9, startpos= 38, endpos= 48, name='versender_berechnugnsplz', fieldclass=FixedField, # default=' '), # dict(length=35, startpos= 48, endpos= 83, name='versender_berechnungsort', fieldclass=FixedField, # default=' '), # dict(length=17, startpos= 83, endpos=100, name='versender_kundennummer', fieldclass=RightAdjustedField, # default='11515'), # dict(length= 9, startpos=100, endpos=109, name='warenwert', fieldclass=DecimalFieldWithoutDot, precision=2), # dict(length= 3, startpos=109, endpos=112, name='waehrung', default='EUR'), # dict(length=13, startpos=112, endpos=123, name='frei', fieldclass=FixedField, default=' '), #] # #doctext = """Empfänger-Adress-Satz - Teil 1""" #Dfelder = [ # dict(length=35, startpos= 0, endpos= 35, name='name1'), # dict(length=35, startpos= 35, endpos= 70, name='name2'), # dict(length=35, startpos= 70, endpos=105, name='stadtteil'), # dict(length=19, startpos=105, endpos=123, name='frei'), #] # #doctext = """Empfänger-Adress-Satz - Teil 2""" #Efelder = [ # dict(length=25, startpos= 0, endpos= 35, name='strasse'), # dict(length= 3, startpos= 35, endpos= 38, name='land'), # dict(length= 9, startpos= 38, endpos= 47, name='plz'), # dict(length=35, startpos= 47, endpos= 81, name='Empfängerort muss'), # dict(length= 3, startpos= 81, endpos= 84, name='Zustellbezirk Empfänger'), # dict(length=10, startpos= 84, endpos= 94, name='Matchcode Empfänger- Nachname'), # dict(length=17, startpos= 94, endpos=111, name='Kunden-Nr. Empfänger'), # dict(length=10, startpos=111, endpos=121, name='Original-ID des VP beim Empfangspartner*'), # dict(length= 2, startpos=123, endpos=123, name='Frei'), #] # #dictext="""Sendungs-Positions-Satz (je Sendungsteil; mehrfach möglich).""" #Ffelder = [ # dict(kength= 4, startpos= 0, endpos= 4, name='packstueck_anzahl'), # dict(kength= 2, startpos= 4, endpos= 6, name='verpackungsart'), # ??? # dict(kength= 4, startpos= 6, endpos= 10, name='packstueck_anzahl_auf_paletten', fieldclass=FixedField, # default=' '), # dict(kength= 2, startpos= 10, endpos= 12, name='verpackungsart_auf_paletten', fieldclass=FixedField, # default=' '), # dict(kength=20, startpos= 10, endpos= 30, name='wareninhalt'), # dict(kength=20, startpos= 30, endpos= 50, name='zeichen', # doc="Zeichen der Sendung beim Versender"), # dict(kength= 5, startpos= 50, endpos= 55, name='gewicht', fieldclass=IntegerField, # doc="Bruttogewicht in KG."), # dict(kength= 5, startpos= 55, endpos= 60, name='gewicht_frachtpflichtig', fieldclass=IntegerField, # doc="In der Regel max(gewicht, N), wobei N=150 oder 200"), # # Die Satzart ‘F’ darf nicht mit einem 2. Sendungsteil versehen werden, # # wenn die Satzarten ‘G’ bzw. ‚Y‘ und ‚Z‘ (Gefahrgut) oder Satzart ‘H’ (Packstück-Nr.) folgen! # dict(kength=68, startpos= 60, endpos=123, name='frei'), #] # Verpackungsartschlüssel (Satzart ‘F’, Positionen 009/010, 015/016, 071/072 und 077/078) # Schlüssel Art # AB Auf Bohlen # AD AD-Bahnbehälter # BD BD-Bahnbehälter # BE Beutel # BL Ballen # BU Bund # CC Collico # CO SystemPlus Collo # CD CD-Bahnbehälter # CP Chep-Palette # DO Dose # DR Drum # EI Eimer # EB Einweg-Behälter # EP Einweg-Palette # FA Fass # FK Faltkiste # FL Flasche # FP DB Euro-Flachpalette # GE Gebinde # GP Gitterboxpalette # GS Gestell # HC Haus-Haus-Corlette # HO Hobbock # HP Halbpalette # KA Kanne # KB Kundeneigener Sonderbehälter # KF Korbflasche # KI Kiste # KN Kanister # KO Korb # KP Kundeneigene Sonderpalette # KS Kasten # KT Karton # PA Paket # PK Pack # RC Rollcontainer # RG Ring # RO Rolle # SA Sack # SB Spediteureigener Behälter # ST Stück # TC Tankcontainer # TR Trommel # UV Unverpackt # VG Verschlag # 'H': '002%(barcode)-35s%(foo)35s%(foo)35s%(foo)16s', # doctext = """Packstück-Nummern-Satz (je Packstück-Nr./Gruppe; mehrfach möglich)""" # # BARCODETYP_CHOICES = { 1: 'Freie, unformatierte Markierung', # 2: 'Nummer der Versandeinheit (EAN 128)', # 3: 'Nummer der Versandeinheit (EAN 128) FORTRAS', # 4: 'Paket-Nummer DPD (2/5 Interleaved)', # 5: 'Router Label-Nummer DPD (2/5 Interleaved)', # 6: 'Packstück-Nummer SystemPlus (2/5 Interleaved)', # 7: 'Router Label-Nummer SystemPlus (2/5 Interleaved)', # 8: 'IDS-Barcode (2/5 Interleaved)', # 9: 'IDS-Barcode (39)', # 10: 'IDS-Barcode (128)', # 11: 'Nummer der Versandeinheit (Philips)', # 12: 'Wechselbehälter-Barcode (2/5 Interleaved)', # 13: 'DPD Container-Nummer (2/5 Interleaved)', # } # Hfelder = [ # dict(length= 3, startpos= 0, endpos= 3, name='barcodeart', fieldclass=IntegerFieldZerotagged, # choices=BARCODETYP_CHOICES), # dict(length=35, startpos= 3, endpos= 38, name='barcode1'), # dict(length=35, startpos= 38, endpos= 73, name='barcode2'), # dict(length=35, startpos= 73, endpos=108, name='barcode3'), # dict(length=16, startpos=108, endpos=123, name='frei'), # ] # # # 'I': ('%(sendungsnummer)-16s%(sendungskilo)05d0000000000%(ladedm)03d %(frankatur)-2s%(frankatur)-2s %(foo)30s %(foo)30s%(foo)16s '), # doctext = """Sendungs-Gewichte (je Sendung 1x)""" # # # Frankaturschlüssel des Spediteur-Übergabescheins (Satzart ‘I’, Positionen 043/044) # # kann Bordero-Frankatur wie folgt ergeben: # FRANKATUR_CHOICES = { 0: 'ohne Berechnung', # 4: 'Frei Empfangsspediteur', # 2: 'frei Haus', # 5: 'frei Haus verzollt', # 6: 'frei Haus unverzollt ', # 7: 'unfrei', # 9: 'Nachlieferung', # } # # # Hinweistextschlüssel (Satzart ‘I’, Positionen 047/048 und 079/080 sowie Satzart ‚T‘ # # Positionen 002/003, 034/035 und 066/067) # # * Exakte Definition der Formate bei Schlüsseln mit Zusatztext: # # 1) Datum (TTMMJJJJ); Feldgröße: 8 Stellen # # 2) Uhrzeit (SSMM); Feldgröße: 4 Stellen # # Die Positionen 049 - 056 enthalten das Datum (TTMMJJJJ), Position # # 057 ist leer, die Positionen 058 - 051 enthalten die Uhrzeit (SSMM). # HINWEISTEXTTYP_CHOICES = { 1: 'Sendung bitte avisieren unter Tel.-Nr.', # 2: 'Recall-Service - Nach Zustellung Rückruf unter Tel.-Nr.', # 3: 'Achtung Zollgut, Anlage Zollversandschein', # 14: 'Termindienst! Auslieferung spätestens am (nicht Eingangstag)', # 15: 'Fixtermin! Nicht früher oder später - am', # 16: 'Termingut! Unbedingt zustellen in KW:', # 17: 'SystemPlus 10-Uhr-Service - Zustellung bis 10.00 Uhr', # 18: 'SystemPlus 12-Uhr-Service - Zustellung bis 12.00 Uhr', # 19: 'SystemPlus Next-Day-Service (24 Std.-Service)', # 20: 'SystemPlus international Express Service', # 21: 'Sendung bitte nur liegend transportieren', # 22: 'Thermogut - vorgegebenen Temperaturbereich beachten', # 23: 'Empfänger kann Regalservice verlangen', # 25: 'Samstag-Service via Night Star Express', # 50: 'Warenwertnachnahme - Quick-Nachnahme', # 51: 'Zustellung unbedingt mit Hebebühnen-LKW', # 52: 'Sendung vor Zustellung telefonisch avisieren', # 53: 'Achtung Warenwertnachnahme - nur gegen bar ausliefern', # 58: 'Warenwertnachnahme - Verrechnungsscheck', # 60: 'Sendung ohne Entladung direkt ausliefern', # 61: 'Empfindliche Ware - vorsichtig behandeln', # 70: 'Lieferscheinnummer', # 71: 'Kundenauftragsnummer', # 72: 'Abholauftragsreferenz', # 73: 'Freie weitere Kundenreferenz', # 74: 'Retourenreferenz', # } # # # # Hinweistextschlüssel der Auftragsart (Satzart ‘I’, Position 127) # # 'I': ('%(sendungsnummer)-16s%(sendungskilo)05d0000000000%(ladedm)03d %(frankatur)-2s%(frankatur)-2s %(foo)30s %(foo)30s%(foo)16s '), # Ifelder = [ # dict(lentgth=16, startpos= 0, endpos= 16, name='Sendungs-Nr. Versandpartner****** muss'), # dict(lentgth= 5, startpos= 16, endpos= 21, name='gewicht', fieldclass=IntegerField, # doc="Sendungsgewicht in KG"), # dict(lentgth= 5, startpos= 21, endpos= 26, name='gewicht_frachtpflichtig', fieldclass=IntegerField, # doc="Frachtpflichtiges Sendungsgewicht in KG"), # dict(lentgth= 5, startpos= 26, endpos= 31, name='kubikdezimeter', fieldclass=IntegerField), # dict(lentgth= 3, startpos= 0, endpos=123, name='lademeter', fieldclass=DecimalFieldWithoutDot, # precision=1), # dict(lentgth= 2, startpos= 0, endpos=123, name='zusaetzliche_ladehilfsmittel', fieldclass=IntegerField), # dict(lentgth= 2, startpos= 0, endpos=123, name='verpackungsart_zusaetzliche_ladehilfsmittel'), # 2 041 - 042 # dict(lentgth= 2, startpos= 0, endpos=123, name='frankatur_fpediteur-Über- gabeschein* muss', choices=FRANKATUR_CHOICES), # 2 043 - 044 # dict(lentgth= 2, startpos= 0, endpos=123, name='Frankatur Bordero* muss', choices=FRANKATUR_CHOICES), # 2 045 - 046 # dict(lentgth= 2, startpos= 0, endpos=123, name='Hinweistextschlüssel 1** kann'), # 2 047 - 048 # dict(lentgth=30, startpos= 0, endpos=123, name='Hinweiszusatztext 1 kann'), # 30 049 - 078 # dict(lentgth= 2, startpos= 0, endpos=123, name='Hinweistextschlüssel 2** kann'), # 2 079 - 080 # dict(lentgth=30, startpos= 0, endpos=123, name='Hinweiszusatztext 2 kann'), # 30 081 - 110 # dict(lentgth=16, startpos= 0, endpos=123, name='Sendungs-Nr. Empfangspartner*** kann'), # 16 111 - 126 # dict(lentgth= 1, startpos=121, endpos=122, name='auftragsart**** kann'), # 1 127 - 127 # dict(lentgth= 1, startpos=122, endpos=123, name='lieferscheindaten_folgen', fieldclass=FixedField, # default=' '), # ] #** siehe beigefügter Hinweistextschlüssel #*** bei Nachlieferungen (Original-Sendungs-Nr. des EP) bzw. bei Ersterfassung von über- #zähligen Sendungen (vorläufige Sendungs-Nr. des EP) #**** siehe beigefügter Schlüsseltabelle für Auftragsarten # 'L': ('%(sendungen)05d%(packstuecke)05d%(bruttogewicht)05d' # + '%(kostensteuerplichtig)09d%(kostensteuerfrei)09d000000000%(kostenzoll)09d' # + '%(eust)09d000%(gitterboxen)03d%(europaletten)03d000000000000' # + '%(sonstigeladehilfsmittel)03d000N%(foo)36s'), # (Muss/Kann) davon dezimal Position # Bordero-Summen-Satz muss # (je Bordero 1x) # Satzart ‘L’ muss 1 001 - 001 # Konstante ‘999’ muss 3/0 002 - 004 # Gesamt-Sendungs-Anzahl muss 5/0 005 - 009 # Gesamt-Packstück-Anzahl muss 5/0 010 - 014 # Tatsächliches Bruttoge- # wicht gesamt in kg muss 5/0 015 - 019 # Gesamt-Empf.-Kosten # steuerpflichtig muss 9/2 020 - 028 # Gesamt-Empf.-Kosten # steuerfrei muss 9/2 029 - 037 # Gesamt-Versendernach- # nahme muss 9/2 038 - 046 # Gesamt-Zoll muss 9/2 047 - 055 # Gesamt-EUSt muss 9/2 056 - 064 # Anzahl SB = spediteur- # eigene Behälter muss 3/0 065 - 067 # Anzahl GP = Gitterbox- # Paletten** muss 3/0 068 - 070 # Anzahl FP = Euro-Flach- # Paletten** muss 3/0 071 - 073 # Anzahl CC = Collico muss 3/0 074 - 076 # Anzahl AD = Bahnbehälter muss 3/0 077 - 079 # Anzahl BD = Bahnbehälter muss 3/0 080 - 082 # Anzahl CD = Bahnbehälter muss 3/0 083 - 085 # Anzahl FP = zusätzliche # Ladehilfsmittel ** muss 3/0 086 - 088 # Anzahl GP = zusätzliche # Ladehilfsmittel** muss 3/0 089 - 091 # Clearing-Kennzeichen (J/N)*muss 1 092 - 092 # Frei 36 093 - 128 # Die Summenfelder für Frachtbeträge, Kosten und Nachnahmen innerhalb dieser Satzart sind # währungsneutral, d.h. es ist lediglich eine Summierung der Werte aus der Satzart K vorzunehmen. # Eine Währungsumrechnung darf nicht erfolgen. # * J = Kosten sind für das Clearing zu berücksichtigen (wird nicht verwendet) # N = Kosten sind nicht für das Clearing zu berücksichtigen (wird nicht verwendet) # # # 'T': ('%(textschluessel1)02s%(hinweistext1)-30s' # + '%(textschluessel2)02s%(hinweistext2)-30s' # + '%(textschluessel3)02s%(hinweistext3)-30s%(foo)28s'), # Textschlüssel-Satz* kann # (je Sendung maximal 3x) # Satzart ‘T’ muss 1 001 - 001 # Laufende Bordero-Position muss 3/0 002 - 004 # Hinweistextschlüssel 1** kann 2 005 - 006 # Hinweiszusatztext 1 kann 30 007 - 036 # Hinweistextschlüssel 2** kann 2 037 - 038 # Hinweiszusatztext 2 kann 30 039 - 068 # Hinweistextschlüssel 3** kann 2 069 - 070 # Hinweiszusatztext 3 kann 30 071 - 100 # Frei # # 'J': '%(zusatztext1)-62s%(zusatztext2)-62s', # Satzart ‘J’ muss 1 001 - 001 # Laufende Bordero-Position muss 3/0 002 - 004 # Zusatztext 1 muss 62 005 - 066 # Zusatztext 2 kann 62 067 - 128 class Bordero(object): """Kapselt die Daten für ein Gefäß (LKW) - Verwendung ähnlich IFTSTAR.""" def __init__(self, empfangspartner='11515'): super(Bordero, self).__init__() self.filename = "" self.empfangspartner = empfangspartner self.lieferungen = [] self.satznummer = 0 self.borderonr = None self.verladung = None self.generated_output = '' # definitions of records self.satzarten = {'A': (u'%(borderonr)018d%(datum)-8s%(versandweg)s %(empfangspartner)-10s %(frachtfuehrer)' + '-13s%(plz)-9s%(ort)-12s%(foo)-49s6'), 'B': u'%(name1)-35s%(name2)-35s%(strasse)-35s%(lkz)-3s%(plz)-9s ', 'C': u'%(ort)-35s%(foo)-3s%(foo)9s%(foo)35s%(kdnnr)17s%(wert)09fEUR%(foo)-13s', 'D': u'%(name1)-35s%(name2)-35s%(foo)-35s%(foo)-19s', 'E': (u'%(strasse)-35s%(lkz)-3s%(plz)-9s%(ort)-35s%(foo)3s%(matchcode)-10s' + '%(kdnnr)17s%(foo)10s%(foo)2s'), 'F': (u'%(anzahlpackstuecke)04d%(verpackungsart)2s0000 %(wareninhalt)-20s' + '%(zeichennr)-20s%(sendungskilo)05d00000%(foo)-62s'), 'H': '002%(barcode)-35s%(foo)35s%(foo)35s%(foo)16s', 'I': (u'%(sendungsnummer)-16s%(sendungskilo)05d0000000000%(ladedm)03d ' + '%(frankatur)-2s%(frankatur)-2s %(foo)30s %(foo)30s%(foo)16s '), 'L': (u'%(sendungen)05d%(packstuecke)05d%(bruttogewicht)05d' + '%(kostensteuerplichtig)09d%(kostensteuerfrei)09d000000000%(kostenzoll)09d' + '%(eust)09d000%(gitterboxen)03d%(europaletten)03d000000000000' + '%(sonstigeladehilfsmittel)03d000N%(foo)36s'), 'T': (u'%(textschluessel1)02s%(hinweistext1)-30s' + '%(textschluessel2)02s%(hinweistext2)-30s' + '%(textschluessel3)02s%(hinweistext3)-30s%(foo)28s'), 'J': u'%(zusatztext1)-62s%(zusatztext2)-62s', } def add_lieferung(self, lieferung): """Adds a lieferung to our Bordero.""" if self.generated_output: raise RuntimeError('tried to add data to an already exported Bordero.') self.lieferungen.append(lieferung) def generate_satz(self, satzart, data): """Helper function to generate output for a Record.""" for key in data.keys(): if isinstance(data[key], str): data[key] = unicode(data[key], 'ascii', errors='ignore') ret = ((u'%s%03d' % (satzart, self.satznummer)) + (self.satzarten[satzart] % data)) if len(ret) != 128: raise RuntimeError('bordero Satz %r kaputt! (len=%r) %r %r' % (satzart, len(ret), ret, data)) return ret def generate_kopfsatz_a(self, verladung=None): """Generates bodero record A - header.""" # Bordro nummer - Fortlaufend? # Verkehrsart - immer LKW? # IDNr des Verrsandpartyners beim Empfangspartern? L515 # Frachtführername - Mäuler? # Ladeeinheitnr - 1 & 2? frei # Plombennummer ? frei if not self.borderonr: raise RuntimeError('No bordero nr set.') data = {'empfangspartner': self.empfangspartner, 'frachtfuehrer': 'Maeuler', 'plz': '42897', 'ort': 'Remscheid', 'versandweg': 'L', 'foo': '', 'datum': time.strftime('%d%m%Y'), 'borderonr': self.borderonr} if verladung: if verladung.spedition == 'Direktfahrt': data['versandweg'] = 'X' return self.generate_satz('A', data) def generate_versendersatz_b(self, lieferung): """Generates bodero record B - first half of sender.""" data = {'name1': 'HUDORA GmbH', 'name2': '', 'strasse': u'Jägerwald 13', 'lkz': 'DE', 'plz': '42897'} return self.generate_satz('B', data) def generate_versendersatz_c(self, lieferung): """Generates bodero record C - second half of sender.""" data = {'ort': 'Remscheid', 'foo': ' ', 'kdnnr': self.empfangspartner, 'wert': 0} return self.generate_satz('C', data) def generate_empfaengersatz_d(self, lieferung): """Generates bodero record D - first half of recipient.""" data = {'name1': _clip(35, lieferung.name1), 'name2': _clip(35, lieferung.name2), 'foo': ' '} return self.generate_satz('D', data) def generate_empfaengersatz_e(self, lieferung): """Generates bodero record E - second half of recipient.""" data = {'strasse': _clip(35, lieferung.adresse), 'lkz': _clip(3, lieferung.land), 'plz': _clip(9, lieferung.plz), 'ort': _clip(35, lieferung.ort), 'kdnnr': lieferung.kundennummer, 'matchcode': _clip(10, (lieferung.name1 + lieferung.ort).replace(' ', '')), 'foo': ' '} return self.generate_satz('E', data) def generate_sendungspossatz_f(self, lieferung): """Generates bodero record F.""" # Packstück Anzahl? Pakete? Paletten? NVEs! # Fuer weitere Paletten typen neuen Satz # Tatsächliches Gewicht einpflegen data = {'anzahlpackstuecke': len(lieferung.packstuecke), 'verpackungsart': _clip(2, 'FP'), 'sendungskilo': int(lieferung.gewicht / 1000), 'wareninhalt': _clip(20, 'HUDORA Sportartikel'), 'zeichennr': _clip(20, '%s/%s' % (lieferung.lieferscheinnummer, lieferung.id)), 'foo': ''} return self.generate_satz('F', data) def generate_packstuecksatz(self, lieferung, packstueck): """Generates bodero record H.""" barcode = packstueck.nve if not barcode.startswith('00'): barcode = '00' + barcode data = {'barcode': barcode, 'foo': ' '} return self.generate_satz('H', data) def generate_sendungsinfosatz_i(self, lieferung): """Generates bodero record I.""" # Sendungsnummer - eindeutig! data = {'sendungsnummer': _clip(16, "%016s" % lieferung.id), 'ladedm': 0, 'frankatur': '02', # frei Haus 'foo': ' '} data['sendungskilo'] = int(lieferung.gewicht / 1000) return self.generate_satz('I', data) def generate_textsatz_t(self, lieferung, schluesselliste): """Generates bodero record(s) T - text info.""" satz = [] ret = [] for schluessel, text in schluesselliste: satz.append((schluessel, text)) if len(satz) == 3: data = {'textschluessel1': '%02d' % int(satz[0][0]), 'hinweistext1': _clip(30, satz[0][1]), 'textschluessel2': '%02d' % int(satz[1][0]), 'hinweistext2': _clip(30, satz[1][1]), 'textschluessel3': '%02d' % int(satz[2][0]), 'hinweistext3': _clip(30, satz[2][1]), 'foo': ' '} satz = [] ret.append(self.generate_satz('T', data)) if len(satz) > 0: data = {'textschluessel1': '%02s' % int(satz[0][0]), 'hinweistext1': _clip(30, satz[0][1]), 'textschluessel2': ' ', 'hinweistext2': '', 'textschluessel3': ' ', 'hinweistext3': '', 'foo': ' '} if len(satz) > 1: data['textschluessel2'] = '%02d' % int(satz[1][0]) data['hinweistext2'] = _clip(30, satz[1][1]) ret.append(self.generate_satz('T', data)) return '\n'.join(ret) def generate_textsaetze(self, lieferung): """Generate as many T records as needed.""" schluesselliste = [] if lieferung.avisieren_unter: # 52 = Sendung vor Zustellung telefonisch avisieren # 01 = Sendung bitte avisieren unter Tel.-Nr.: ...(Zusatztext) schluesselliste.append(('01', _clip(30, lieferung.avisieren_unter))) if lieferung.fixtermin: # 15 = Fixtermin! Nicht früher oder später - am: ... (Zusatztext)* datum = lieferung.fixtermin.strftime('%d%m%Y') zeit = lieferung.fixtermin.strftime('%H:%M') if zeit == '00:00': zeit = ' ' timestamp = '%8s %5s' % (datum, zeit) schluesselliste.append(('15', _clip(30, timestamp))) if lieferung.hebebuehne: # 51 = Zustellung unbedingt mit Hebebühnen-LKW schluesselliste.append(('51', _clip(30, 'Zustellung mit Hebebühne'))) if lieferung.auftragsnummer_kunde.strip(): # 71 = Kundenauftragsnummer (Nummer im Zusatztext) schluesselliste.append(('71', _clip(30, lieferung.auftragsnummer_kunde))) # 70 = Lieferscheinnummer (Nummer im Zusatztext) schluesselliste.append(('70', _clip(30, lieferung.lieferscheinnummer))) # 73 = Freie weitere Kundenreferenz (Referenz im Zusatztext) # schluesselliste.append(('73', _clip(30, 'http://hudora.de/track/XXX'))) # 14 = Termindienst! Auslieferung spätestens am (nicht Eingangstag): (Zusatztext)* # 16 = Termingut! Unbedingt zustellen in KW: ... (Zusatztext) # 61 = Empfindliche Ware - vorsichtig behandeln return self.generate_textsatz_t(lieferung, schluesselliste) def generate_zusatztextsatz_j(self, lieferung): """Generates bodero record J - additional text info.""" data = {'zusatztext1': _clip(62, u'AuftragsNr: %s / KundenNr: %s' % (lieferung.auftragsnummer, lieferung.kundennummer)), 'zusatztext2': _clip(62, u'huLOG Code: %s' % lieferung.code), 'foo': ' '} return self.generate_satz('J', data) def generate_summensatz_l(self): """Generates bodero record L.""" # Was ist ein Packstück? => NVEs # Empfangskosten? # Was bei Einwegpaletten? # Was bei CHEP / Düsseldorfer Paletten = speditereigenebehaelter # clearing kennzeichen? data = {'sendungen': len(self.lieferungen), 'bruttogewicht': int(sum([lieferung.gewicht for lieferung in self.lieferungen]) / 1000), 'kostensteuerplichtig': 0, 'kostensteuerfrei': 0, 'kostenzoll': 0, 'eust': 0, 'gitterboxen': 0, 'europaletten': sum([len(lieferung.packstuecke) for lieferung in self.lieferungen]), 'packstuecke': sum([len(lieferung.packstuecke) for lieferung in self.lieferungen]), 'sonstigeladehilfsmittel': 0, 'foo': '', } self.satznummer = 999 return self.generate_satz('L', data) def generate_lieferungssaetze(self, lieferung): """Generates all bodero records for a shipment.""" self.satznummer += 1 out = [] out.append(self.generate_versendersatz_b(lieferung)) out.append(self.generate_versendersatz_c(lieferung)) out.append(self.generate_empfaengersatz_d(lieferung)) out.append(self.generate_empfaengersatz_e(lieferung)) out.append(self.generate_sendungspossatz_f(lieferung)) for packstueck in lieferung.packstuecke: out.append(self.generate_packstuecksatz(lieferung, packstueck)) out.append(self.generate_sendungsinfosatz_i(lieferung)) out.append(self.generate_textsaetze(lieferung)) out.append(self.generate_zusatztextsatz_j(lieferung)) return u'\n'.join(out) def generate_dataexport(self): """Return complete BODERO data.""" if not self.generated_output: out = [] out.append(self.generate_kopfsatz_a(self.verladung)) for lieferung in self.lieferungen: out.append(self.generate_lieferungssaetze(lieferung)) out.append(self.generate_summensatz_l()) self.generated_output = (u"@@PHBORD128 0128003500107 HUDORA1 MAEULER\n" + u'\n'.join(out) + u'\n@@PT\n') return self.generated_output def ship(verladung, empfangspartner='11515', basedir='/usr/local/maeuler/current/In/BORD/'): """Creates a BORDERO object and writes it to a file. The file lies in directory basedir containing the borderonr and a timestamp in its name. Returns the BORDERO object w/ its filename attached. """ bordero = Bordero(empfangspartner) bordero.verladung = verladung bordero.borderonr = verladung.borderonr for lieferung in verladung.lieferungen: bordero.add_lieferung(lieferung) data = bordero.generate_dataexport() # generate first to assure bordero.borderonr is set # we first create a temporary file and later rename it to it's final name basefilename = time.strftime('%Y%m%dT%H%M%S') + ('_%05d.txt' % (bordero.borderonr)) tmpfilename = os.path.join(basedir, '._' + basefilename + '_tmp') filename = os.path.join(basedir, basefilename) outfile = open(tmpfilename, 'w') outfile.write(data.encode('latin-1', 'ignore')) outfile.close() os.rename(tmpfilename, filename) for lieferung in verladung.lieferungen: lieferung.ship() lieferung.log(code=201, message="Verladung abgeschlossen / DFü an Spedition (Mäuler)") lieferung.save() verladung.ship() bordero.filename = filename return bordero def ship_lieferungen(lieferungen, empfangspartner='11515'): """Should be called when 'lieferungen' just left the building.""" bordero = Bordero(empfangspartner) for lieferung in lieferungen: bordero.add_lieferung(lieferung) data = bordero.generate_dataexport() # generate first to assure bordero.borderonr is set # we first create a temporary file and later rename it to it's finaal name filename = time.strftime('%Y%m%dT%H%M%S') + ('_%05d.txt' % (bordero.borderonr)) outfile = open(os.path.join('/usr/local/maeuler/current/In/BORD/', '._' + filename + '_tmp'), 'w') outfile.write(data) outfile.close() os.rename(os.path.join('/usr/local/maeuler/current/In/BORD/', '._' + filename + '_tmp'), os.path.join('/usr/local/maeuler/current/In/BORD/', filename)) for lieferung in lieferungen: lieferung.ship() lieferung.log(code=201, message="DFü an Mäuler") lieferung.save() return bordero ================================================ FILE: pyshipping/fortras/entl.py ================================================ #!/usr/bin/env python # encoding: utf-8 """ entl.py - parse Fortras ENTL messages. Created by Maximillian Dornseif on 2006-11-19. You may consider this BSD licensed. """ import re import logging import datetime class Entladebericht(object): """Parses and represent an ENTL message.""" m_record_re = (r'M(?P[0-9 ]{18})(?P[0-9 ]{8})(?P.{10})' + r'(?P[0-9 ]{8})(?P[0-9 ]{4})(?P..)' + r'(?P[0-9 ]{8})(?P[0-9 ]{4})' + r'(?P[0-9 ]{8})(?P[0-9 ]{4})' + r'(?P.{30}) (?P.{9})5') m_record_re = re.compile(m_record_re) # Satzart ‘M’ muss 1 001 - 001 # Bordero-Nr.Versandpartner muss 18 002 - 019 # Borderodatum Versandpartner (TTMMJJJJ) muss 8/0 020 - 027 # Kunden-Nr. Empfangspartner beim Versandpartner muss 10 028 - 037 # Eingangsdatum (TTMMJJJJ) muss 8/0 038 - 045 # Eingangszeit (SSMM) muss 4/0 046 - 049 # Zeitschranken-Status* muss 2 050 - 051 # Entladebeginn-Datum (TTMMJJJJ) muss 8/0 052 - 059 # Entladebeginn Zeit (SSMM) muss 4/0 060 - 063 # Entladeende-Datum (TTMMJJJJ) muss 8/0 064 - 071 # Entladeende-Zeit (SSMM) muss 4/0 072 - 075 # Nicht sendungsbezogener Entladehinweis-Text kann 30 076 - 105 # Euro-Paletten-Belastung Versandpartner aus Bordero kann 3/0 106 - 108 # Gitterbox-Paletten- Belastung Versandpartner aus Bordero kann 3/0 109 - 111 # Euro-Paletten-Entladung Empfangspartner kann 3/0 112 - 114 # Gitterbox-Paletten-Entladung Empfangspartner kann 3/0 115 - 117 # Verkehrsart** kann 1 118 - 118 # Frei 9 119 - 127 # Releasestand 5’ muss 1 128 - 128 n_record_re = (r'N(?P[0-9 ]{18})(?P[0-9 ]{3})(?P[0-9 ]{16})' + r'(?P[0-9 ]{16})(?P..)' + r'(?P[0-9 ]{4})(?P..)(?P.{29})' + r'(?P..)(?P[0-9 ]{4})(?P..)' + r'(?P.{29})') n_record_re = re.compile(n_record_re) # Satzart ‘N’ muss 1 001 - 001 # Bordero-Nr. Versandpartner muss 18 002 - 019 # Laufende Bordero-Position Versandpartner muss 3/0 020 - 022 # Sendungs-Nr. Versandpartner muss 16 023 - 038 # Sendungs-Nr. Empfangspartner muss 16 039 - 054 # Differenzartschlussel 1* muss 2 055 - 056 # Differenzanzahl 1 kann 4/0 057 - 060 # Verpackungsart 1** kann 2 061 - 062 # Text/Hinweis 1 kann 29 063 - 091 # Differenzartschlussel 2* kann 2 092 - 093 # Differenzanzahl 2 kann 4/0 094 - 097 # Verpackungsart 2** kann 2 098 - 099 # Text/Hinweis 2 kann 29 100 - 128 # no match 'V461 720-00 00340498430009431112 0 19092008183100 ' v_record_re = (r'V(?P[0-9 ]{18})(?P.{16})(?P...)' + r'(?P.{35})(?P...)(?P.{24})' + r'(?P[0-9 ]{8})(?P