diff --git a/constants.py b/constants.py index 18c2019..09d71f8 100644 --- a/constants.py +++ b/constants.py @@ -1,5 +1,15 @@ import os import sys +from datetime import datetime +from pathlib import Path + +from jinja2 import FileSystemLoader, Environment + +from utils.code128 import encode128 + +ENV = Environment(loader=FileSystemLoader(Path("templates") / Path("documents"))) +ENV.globals['now'] = datetime.now +ENV.globals['encode128'] = encode128 APP_PATH = os.path.dirname(sys.executable) if getattr(sys, 'frozen', False) else os.path.dirname(__file__) @@ -29,3 +39,13 @@ MONTHS = ( 'ноября', 'декабря' ) + +DEAL_STATUS_STR = [ + 'Создан', + 'Ожидает принятия', + 'Упаковка', + 'Ожидание отправки', + 'Ожидание оплаты', + 'Завершено', + 'Отменено', +] diff --git a/routers/deal.py b/routers/deal.py index 5d3d43c..04d1d57 100644 --- a/routers/deal.py +++ b/routers/deal.py @@ -189,6 +189,20 @@ async def get_deal_document( pdf_file: BytesIO = await BillingService(session).create_billing_document_pdf(deal_id) return Response(pdf_file.getvalue(), media_type='application/pdf') + +@deal_router.get( + '/detailedDocument/{deal_id}', + operation_id='get_deal_document_detailed', + # dependencies=[Depends(authorized_user)], +) +async def get_detailed_deal_document( + deal_id: int, + session: Annotated[AsyncSession, Depends(get_session)], +): + pdf_file: BytesIO = await DealService(session).create_detailed_deal_document_pdf(deal_id) + return Response(pdf_file.getvalue(), media_type='application/pdf') + + # endregion # region Deal services diff --git a/services/billing.py b/services/billing.py index e426e8c..0e442ef 100644 --- a/services/billing.py +++ b/services/billing.py @@ -11,7 +11,7 @@ from starlette import status from weasyprint import HTML import backend.config -from constants import MONTHS +from constants import MONTHS, ENV from external.billing import BillingClient, CreateBillingRequestValue, CreateBillRequestSchema, CreateBillRequestItems, \ BillStatusUpdateRequest, NotificationChannel, NotifyReceivedBillRequestSchema, DeleteBillRequestSchema from models import DealBillRequest, Deal, DealProduct, DealService as DealServiceModel @@ -20,8 +20,6 @@ from services.base import BaseService from services.deal import DealService from utils.list_utils import to_locale_number -env = Environment(loader=FileSystemLoader(Path("templates") / Path("documents"))) - class BillingService(BaseService): async def _process_update_details( @@ -182,7 +180,7 @@ class BillingService(BaseService): deal_price = sum((service.price * service.amount for service in services)) deal_price_words = get_string_by_number(deal_price)[0:-10] deal_price = to_locale_number(deal_price) - template = env.get_template("bill-of-payment.html") + template = ENV.get_template("bill-of-payment.html") now = datetime.datetime.now() curr_date = f"{now.day} {MONTHS[now.month - 1]} {now.year} г." diff --git a/services/deal.py b/services/deal.py index 904fc56..501e828 100644 --- a/services/deal.py +++ b/services/deal.py @@ -1,12 +1,17 @@ +from io import BytesIO + import lexorank from fastapi import HTTPException from sqlalchemy import select, func, update, delete, insert from sqlalchemy.orm import joinedload, selectinload from starlette import status +from weasyprint import HTML, CSS +import constants import models.deal import models.secondary -from models import User, Service, Client, DealProduct +from constants import ENV, DEAL_STATUS_STR +from models import User, Service, Client, DealProduct, DealService as DealServiceModel, Product from models.deal import * from schemas.client import ClientDetailsSchema from schemas.deal import * @@ -15,6 +20,7 @@ from services.base import BaseService from services.client import ClientService from services.service import ServiceService from services.shipping_warehouse import ShippingWarehouseService +from utils.images_fetcher import fetch_images class DealService(BaseService): @@ -987,3 +993,56 @@ class DealService(BaseService): except Exception as e: await self.session.rollback() return DealCompleteResponse(ok=False, message=str(e)) + + # region Deal documents + + @staticmethod + async def _get_product_services_totals(deal: Deal) -> List[int]: + totals: List[int] = [] + + for product in deal.products: + total = sum((service.price * product.quantity for service in product.services)) + totals.append(total) + + return totals + + async def _create_detailed_deal_document_html(self, deal_id: int): + deal: Deal | None = await self.session.scalar( + select(Deal) + .where(Deal.id == deal_id) + .options( + selectinload(Deal.products).selectinload(DealProduct.services), + selectinload(Deal.products).selectinload(DealProduct.product).selectinload(Product.barcodes), + selectinload(Deal.services).selectinload(DealServiceModel.service), + selectinload(Deal.status_history), + joinedload(Deal.client), + joinedload(Deal.shipping_warehouse), + ) + ) + + if not deal: + return "" + + last_status: DealStatusHistory = max(deal.status_history, key=lambda status: status.changed_at) + general_services_total = sum((service.price * service.quantity for service in deal.services)) + product_services_totals = await self._get_product_services_totals(deal) + template = ENV.get_template("deal.html") + product_images = await fetch_images([product.product.images[0].image_url for product in deal.products]) + + result = template.render({ + "general_services_total": general_services_total, + "product_services_totals": product_services_totals, + "deal": deal, + "current_status_str": DEAL_STATUS_STR[deal.current_status], + "last_status": last_status, + "product_images": product_images, + }) + return result + + async def create_detailed_deal_document_pdf(self, deal_id) -> BytesIO: + doc = await self._create_detailed_deal_document_html(deal_id) + pdf_file = BytesIO() + HTML(string=doc).write_pdf(pdf_file, stylesheets=[CSS(constants.APP_PATH + '/static/css/deal.css')]) + return pdf_file + + # endregion diff --git a/static/css/deal.css b/static/css/deal.css new file mode 100644 index 0000000..3e116ff --- /dev/null +++ b/static/css/deal.css @@ -0,0 +1,167 @@ +@media print { + #header, + #footer, + #nav { + display: none !important; + } +} + +hr { + background-color: black; + height: 2px; + border: none; + margin-left: -100px; + margin-right: -100px; +} + +.doc-container { + margin: -30px 0; + padding: 0 15px; + font-size: 13px; + font-family: sans-serif; +} + +.deal-name-id-container { + justify-content: space-between; + display: flex; + align-items: center; +} + +.deal-id { + border: black solid 2px; + padding: 3px 2px; + margin: 0; +} + +.total { + padding: 10px 0; +} + +.small-text { + font-size: 12px; + margin-bottom: 5px; +} + +.medium-text { + font-size: 15px; + margin-bottom: 5px; +} + +.large-text { + font-size: 21px; + margin-bottom: 5px; +} + +.bold { + font-weight: bold; +} + +.product-data { + height: 160px; + display: flex; + flex-wrap: wrap; + flex-direction: column; +} + +.product-data > * { + width: 170px; + padding-right: 170px; +} + +table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + border: solid gray 2px; + border-radius: 15px; +} + +thead { + display: table-header-group; +} + +tfoot { + display: table-footer-group; +} + +td:nth-child(5) { + border-right: 1px solid white; +} + +table tbody tr:nth-child(odd) { + background-color: #e5e5e5; +} + +tr { + page-break-inside: avoid; +} + +td, th { + padding: 10px 13px; +} + +td:first-child, th:first-child { + padding-left: 50px; +} + +td:last-child, th:last-child { + padding-right: 50px; +} + +td:first-child, th:first-child { + text-align: left; +} + +td:first-child { + white-space: normal; + word-wrap: break-word; +} + +td:not(:first-child), th:not(:first-child) { + text-align: right; +} + +td:not(:first-child) { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +table tfoot { + border-top: 2px solid black; +} + +.align-right { + text-align: right; +} + +.img-container { + margin: -30px 30px -45px 0; +} + +.product-img { + height: 300px; + width: 200px; + object-fit: cover; + padding: 0; + margin: 0; + border: black solid 2px; + border-radius: 5px; +} + +.barcode-container { + margin: 15px 0 -5px 385px; + display: flex; + flex-direction: column; + align-items: center; +} + +.barcode { + font-family: "Libre Barcode 128", cursive; + font-size: 30pt; + transform: scale(1, 1.5); +} + +.barcode-text { + margin-top: -10px; +} diff --git a/static/js/JsBarcode.all.min.js b/static/js/JsBarcode.all.min.js new file mode 100644 index 0000000..c584536 --- /dev/null +++ b/static/js/JsBarcode.all.min.js @@ -0,0 +1,2 @@ +/*! JsBarcode v3.11.5 | (c) Johan Lindell | MIT license */ +!function(t){var e={};function n(r){if(e[r])return e[r].exports;var o=e[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:r})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var o in t)n.d(r,o,function(e){return t[e]}.bind(null,o));return r},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=15)}([function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});e.default=function t(e,n){!function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}(this,t),this.data=e,this.text=n.text||e,this.options=n}},function(t,e,n){"use strict";var r;function o(t,e,n){return e in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}Object.defineProperty(e,"__esModule",{value:!0});var i=e.SET_A=0,a=e.SET_B=1,u=e.SET_C=2,f=(e.SHIFT=98,e.START_A=103),c=e.START_B=104,s=e.START_C=105;e.MODULO=103,e.STOP=106,e.FNC1=207,e.SET_BY_CODE=(o(r={},f,i),o(r,c,a),o(r,s,u),r),e.SWAP={101:i,100:a,99:u},e.A_START_CHAR=String.fromCharCode(208),e.B_START_CHAR=String.fromCharCode(209),e.C_START_CHAR=String.fromCharCode(210),e.A_CHARS="[\0-_È-Ï]",e.B_CHARS="[ -È-Ï]",e.C_CHARS="(Ï*[0-9]{2}Ï*)",e.BARS=[11011001100,11001101100,11001100110,10010011e3,10010001100,10001001100,10011001e3,10011000100,10001100100,11001001e3,11001000100,11000100100,10110011100,10011011100,10011001110,10111001100,10011101100,10011100110,11001110010,11001011100,11001001110,11011100100,11001110100,11101101110,11101001100,11100101100,11100100110,11101100100,11100110100,11100110010,11011011e3,11011000110,11000110110,10100011e3,10001011e3,10001000110,10110001e3,10001101e3,10001100010,11010001e3,11000101e3,11000100010,10110111e3,10110001110,10001101110,10111011e3,10111000110,10001110110,11101110110,11010001110,11000101110,11011101e3,11011100010,11011101110,11101011e3,11101000110,11100010110,11101101e3,11101100010,11100011010,11101111010,11001000010,11110001010,1010011e4,10100001100,1001011e4,10010000110,10000101100,10000100110,1011001e4,10110000100,1001101e4,10011000010,10000110100,10000110010,11000010010,1100101e4,11110111010,11000010100,10001111010,10100111100,10010111100,10010011110,10111100100,10011110100,10011110010,11110100100,11110010100,11110010010,11011011110,11011110110,11110110110,10101111e3,10100011110,10001011110,10111101e3,10111100010,11110101e3,11110100010,10111011110,10111101110,11101011110,11110101110,11010000100,1101001e4,11010011100,1100011101011]},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});e.SIDE_BIN="101",e.MIDDLE_BIN="01010",e.BINARIES={L:["0001101","0011001","0010011","0111101","0100011","0110001","0101111","0111011","0110111","0001011"],G:["0100111","0110011","0011011","0100001","0011101","0111001","0000101","0010001","0001001","0010111"],R:["1110010","1100110","1101100","1000010","1011100","1001110","1010000","1000100","1001000","1110100"],O:["0001101","0011001","0010011","0111101","0100011","0110001","0101111","0111011","0110111","0001011"],E:["0100111","0110011","0011011","0100001","0011101","0111001","0000101","0010001","0001001","0010111"]},e.EAN2_STRUCTURE=["LL","LG","GL","GG"],e.EAN5_STRUCTURE=["GGLLL","GLGLL","GLLGL","GLLLG","LGGLL","LLGGL","LLLGG","LGLGL","LGLLG","LLGLG"],e.EAN13_STRUCTURE=["LLLLLL","LLGLGG","LLGGLG","LLGGGL","LGLLGG","LGGLLG","LGGGLL","LGLGLG","LGLGGL","LGGLGL"]},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=n(2);e.default=function(t,e,n){var o=t.split("").map((function(t,n){return r.BINARIES[e[n]]})).map((function(e,n){return e?e[t[n]]:""}));if(n){var i=t.length-1;o=o.map((function(t,e){return e=200){i=t.shift()-105;var a=u.SWAP[i];void 0!==a?o=e.next(t,n+1,a):(r!==u.SET_A&&r!==u.SET_B||i!==u.SHIFT||(t[0]=r===u.SET_A?t[0]>95?t[0]-96:t[0]:t[0]<32?t[0]+96:t[0]),o=e.next(t,n+1,r))}else i=e.correctIndex(t,r),o=e.next(t,n+1,r);var f=i*n;return{result:e.getBar(i)+o.result,checksum:f+o.checksum}}}]),e}(a.default);e.default=f},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.mod10=function(t){for(var e=0,n=0;n10*n.width?10*n.width:n.fontSize,r.guardHeight=n.height+r.fontSize/2+n.textMargin,r}return function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}(e,t),r(e,[{key:"encode",value:function(){return this.options.flat?this.encodeFlat():this.encodeGuarded()}},{key:"leftText",value:function(t,e){return this.text.substr(t,e)}},{key:"leftEncode",value:function(t,e){return(0,i.default)(t,e)}},{key:"rightText",value:function(t,e){return this.text.substr(t,e)}},{key:"rightEncode",value:function(t,e){return(0,i.default)(t,e)}},{key:"encodeGuarded",value:function(){var t={fontSize:this.fontSize},e={height:this.guardHeight};return[{data:o.SIDE_BIN,options:e},{data:this.leftEncode(),text:this.leftText(),options:t},{data:o.MIDDLE_BIN,options:e},{data:this.rightEncode(),text:this.rightText(),options:t},{data:o.SIDE_BIN,options:e}]}},{key:"encodeFlat",value:function(){return{data:[o.SIDE_BIN,this.leftEncode(),o.MIDDLE_BIN,this.rightEncode(),o.SIDE_BIN].join(""),text:this.text}}}]),e}(a(n(0)).default);e.default=u},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=function(){function t(t,e){for(var n=0;n10*n.width?r.fontSize=10*n.width:r.fontSize=n.fontSize,r.guardHeight=n.height+r.fontSize/2+n.textMargin,r}return function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}(e,t),r(e,[{key:"valid",value:function(){return-1!==this.data.search(/^[0-9]{12}$/)&&this.data[11]==u(this.data)}},{key:"encode",value:function(){return this.options.flat?this.flatEncoding():this.guardedEncoding()}},{key:"flatEncoding",value:function(){var t="";return t+="101",t+=(0,o.default)(this.data.substr(0,6),"LLLLLL"),t+="01010",t+=(0,o.default)(this.data.substr(6,6),"RRRRRR"),{data:t+="101",text:this.text}}},{key:"guardedEncoding",value:function(){var t=[];return this.displayValue&&t.push({data:"00000000",text:this.text.substr(0,1),options:{textAlign:"left",fontSize:this.fontSize}}),t.push({data:"101"+(0,o.default)(this.data[0],"L"),options:{height:this.guardHeight}}),t.push({data:(0,o.default)(this.data.substr(1,5),"LLLLL"),text:this.text.substr(1,5),options:{fontSize:this.fontSize}}),t.push({data:"01010",options:{height:this.guardHeight}}),t.push({data:(0,o.default)(this.data.substr(6,5),"RRRRR"),text:this.text.substr(6,5),options:{fontSize:this.fontSize}}),t.push({data:(0,o.default)(this.data[11],"R")+"101",options:{height:this.guardHeight}}),this.displayValue&&t.push({data:"00000000",text:this.text.substr(11,1),options:{textAlign:"right",fontSize:this.fontSize}}),t}}]),e}(i(n(0)).default);function u(t){var e,n=0;for(e=1;e<11;e+=2)n+=parseInt(t[e]);for(e=0;e<11;e+=2)n+=3*parseInt(t[e]);return(10-n%10)%10}e.default=a},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r,o=function(){function t(t,e){for(var n=0;n0?e.fontSize+e.textMargin:0)+e.marginTop+e.marginBottom}function u(t,e,n){if(n.displayValue&&ee&&(e=t[n].height);return e},e.getEncodingHeight=a,e.getBarcodePadding=u,e.calculateEncodingAttributes=function(t,e,n){for(var r=0;r=i(t);return e+String.fromCharCode(r?206:205)+u(t,r)}e.default=function(t){var e=void 0;if(a(t).length>=2)e=r.C_START_CHAR+f(t);else{var n=o(t)>i(t);e=(n?r.A_START_CHAR:r.B_START_CHAR)+u(t,n)}return e.replace(/[\xCD\xCE]([^])[\xCD\xCE]/,(function(t,e){return String.fromCharCode(203)+e}))}},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r,o=function(){function t(t,e){for(var n=0;n10*n.width?r.fontSize=10*n.width:r.fontSize=n.fontSize,r.guardHeight=n.height+r.fontSize/2+n.textMargin,r}return function(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}(e,t),r(e,[{key:"valid",value:function(){return this.isValid}},{key:"encode",value:function(){return this.options.flat?this.flatEncoding():this.guardedEncoding()}},{key:"flatEncoding",value:function(){var t="";return t+="101",t+=this.encodeMiddleDigits(),{data:t+="010101",text:this.text}}},{key:"guardedEncoding",value:function(){var t=[];return this.displayValue&&t.push({data:"00000000",text:this.text[0],options:{textAlign:"left",fontSize:this.fontSize}}),t.push({data:"101",options:{height:this.guardHeight}}),t.push({data:this.encodeMiddleDigits(),text:this.text.substring(1,7),options:{fontSize:this.fontSize}}),t.push({data:"010101",options:{height:this.guardHeight}}),this.displayValue&&t.push({data:"00000000",text:this.text[7],options:{textAlign:"right",fontSize:this.fontSize}}),t}},{key:"encodeMiddleDigits",value:function(){var t=this.upcA[0],e=this.upcA[this.upcA.length-1],n=s[parseInt(e)][parseInt(t)];return(0,o.default)(this.middleDigits,n)}}]),e}(i.default);function p(t,e){for(var n=parseInt(t[t.length-1]),r=c[n],o="",i=0,u=0;u=3&&this.number<=131070}}]),e}(((r=i)&&r.__esModule?r:{default:r}).default);e.pharmacode=a},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0}),e.codabar=void 0;var r,o=function(){function t(t,e){for(var n=0;n0?(n=0,o.textAlign="left"):"right"==t.textAlign?(n=e.width-1,o.textAlign="right"):(n=e.width/2,o.textAlign="center"),o.fillText(e.text,n,r))}},{key:"moveCanvasDrawing",value:function(t){this.canvas.getContext("2d").translate(t.width,0)}},{key:"restoreCanvas",value:function(){this.canvas.getContext("2d").restore()}}]),t}();e.default=f},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r,o=function(){function t(t,e){for(var n=0;n0&&(this.drawRect(a-e.width*i,r,e.width*i,e.height,t),i=0);i>0&&this.drawRect(a-e.width*(i-1),r,e.width*i,e.height,t)}},{key:"drawSVGText",value:function(t,e,n){var r,o,i=this.document.createElementNS(f,"text");e.displayValue&&(i.setAttribute("style","font:"+e.fontOptions+" "+e.fontSize+"px "+e.font),o="top"==e.textPosition?e.fontSize-e.textMargin:e.height+e.textMargin+e.fontSize,"left"==e.textAlign||n.barcodePadding>0?(r=0,i.setAttribute("text-anchor","start")):"right"==e.textAlign?(r=n.width-1,i.setAttribute("text-anchor","end")):(r=n.width/2,i.setAttribute("text-anchor","middle")),i.setAttribute("x",r),i.setAttribute("y",o),i.appendChild(this.document.createTextNode(n.text)),t.appendChild(i))}},{key:"setSvgAttributes",value:function(t,e){var n=this.svg;n.setAttribute("width",t+"px"),n.setAttribute("height",e+"px"),n.setAttribute("x","0px"),n.setAttribute("y","0px"),n.setAttribute("viewBox","0 0 "+t+" "+e),n.setAttribute("xmlns",f),n.setAttribute("version","1.1"),n.setAttribute("style","transform: translate(0,0)")}},{key:"createGroup",value:function(t,e,n){var r=this.document.createElementNS(f,"g");return r.setAttribute("transform","translate("+t+", "+e+")"),n.appendChild(r),r}},{key:"setGroupOptions",value:function(t,e){t.setAttribute("style","fill:"+e.lineColor+";")}},{key:"drawRect",value:function(t,e,n,r,o){var i=this.document.createElementNS(f,"rect");return i.setAttribute("x",t),i.setAttribute("y",e),i.setAttribute("width",n),i.setAttribute("height",r),o.appendChild(i),i}}]),t}();e.default=c},function(t,e,n){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var r=function(){function t(t,e){for(var n=0;n + + + + + + + Сделка + + + + +
+
+
+ +
+
+ Название сделки: {{ deal.name }} +
+
+ ID:{{ deal.id }} +
+
+
+
+ Дата создания: {{ now().strftime("%d.%m.%Y, %H:%M") }} +
+
+ Текущий статус: {{ current_status_str }} +
+
+ {% if deal.comment %} + Комментарий к сделке: {{ deal.comment }} + {% else %} + Комментарий к сделке отсутствует + {% endif %} +
+
+ + +
+ Клиент: {{ deal.client.name }} +
+
+ Дата отгрузки: {{ last_status.next_status_deadline.strftime("%d.%m.%Y") }} +
+ {% if deal.base_marketplace.name %} +
Маркетплейс: {{ deal.base_marketplace.name }}
+ {% endif %} + {% if deal.shipping_warehouse.name %} +
Склад отгрузки: {{ deal.shipping_warehouse.name }}
+ {% endif %} + + + + + + + + + + + + {% for service in deal.services %} + + + + + + {% endfor %} + +
Общие услугиКоличествоСумма
{{ service.service.name }}{{ '{:,}'.format(service.quantity) }} шт.{{ '{:,}'.format(service.price * service.quantity).replace(',', ' ') }} Р
+
+ Итого: {{ general_services_total }} Р +
+ + + {% for product in deal.products %} +
+
+
+ {% if product.product.images|length != 0 %} +
+ Фото товара +
+ {% endif %} +
+
+ {{ loop.index }}. {{ product.product.name }} +
+
+
Артикул: {{ product.product.article }}
+ {% if product.product.brand %} +
Бренд: {{ product.product.brand }}
+ {% endif %} + {% if product.product.color %} +
Цвет: {{ product.product.color }}
+ {% endif %} + {% if product.product.size %} +
Размер: {{ product.product.size }}
+ {% endif %} + {% if product.quantity %} +
Количество: {{ product.quantity }}
+ {% endif %} + {% if product.product.additional_info %} +
+ Доп. информация: {{ product.product.additional_info }} +
+ {% endif %} +
+
+
+
+
+
+
+ {{ encode128(product.product.barcodes[0].barcode, "A") }} +
+
+ {{ product.product.barcodes[0].barcode }} +
+
+
+
+ + + + + + + + + + + + {% for service in product.services %} + + + + + + {% endfor %} + +
Наименование услугиЦенаСумма
{{ service.service.name }}{{ '{:,}'.format(product.quantity) }} Р{{ '{:,}'.format(service.price * product.quantity).replace(',', ' ') }} Р
+
+ Итого: {{ product_services_totals[loop.index0] }} Р +
+ {% endfor %} + +
+
+
+ + \ No newline at end of file diff --git a/utils/code128.py b/utils/code128.py new file mode 100644 index 0000000..f47a892 --- /dev/null +++ b/utils/code128.py @@ -0,0 +1,35 @@ +import re + +def to_set_c(text): + # Match pairs of digits and map them to the appropriate ASCII characters + return ''.join([ + chr(int(ascii_code) + 100 if int(ascii_code) > 94 else int(ascii_code) + 32) + for ascii_code in re.findall(r'\d{2}', text) + ]) + +def check_sum_128(data, start_code): + sum_value = start_code + for i, char in enumerate(data): + code = ord(char) + value = code - 100 if code > 199 else code - 32 + sum_value += (i + 1) * value + + checksum = (sum_value % 103) + 32 + if checksum > 126: + checksum += 68 + return chr(checksum) + +def encode128(text, code_abc="B"): + start_code = chr(ord(code_abc.upper()) + 138) + stop = chr(206) + + if code_abc.upper() == 'C': + text = to_set_c(text) + + check = check_sum_128(text, ord(start_code) - 100) + + # Replace spaces with ASCII 194 + text = text.replace(" ", chr(194)) + + return start_code + text + check + stop + diff --git a/utils/images_fetcher.py b/utils/images_fetcher.py new file mode 100644 index 0000000..62a5538 --- /dev/null +++ b/utils/images_fetcher.py @@ -0,0 +1,20 @@ +import asyncio +import base64 +from typing import List, Tuple + +import aiohttp + + +async def fetch_image(url: str) -> str: + async with aiohttp.ClientSession() as session: + async with session.get(url) as response: + if response.status == 200: + image_data = await response.read() + return base64.b64encode(image_data).decode("utf-8") + else: + return "" + + +async def fetch_images(urls: List[str]) -> Tuple[str]: + tasks = [fetch_image(url) for url in urls] + return await asyncio.gather(*tasks)