From 3cdcb239c3a7f49193959fcbb546d37f3b77065d Mon Sep 17 00:00:00 2001 From: Christian Anetzberger Date: Wed, 6 Sep 2023 13:45:04 +0000 Subject: [PATCH 1/2] Rework --- manufacturing_overview/__init__.py | 3 +- manufacturing_overview/hooks.py | 6 +- .../manufacturing_overview/api.py | 65 +++++++++++++------ .../manufacturing_overview/productionplan.py | 59 +++++++++++++++++ manufacturing_overview/public/build.json | 5 -- .../js/manufacturing_overview.bundle.js | 4 ++ .../public/js/manufacturing_overview.min.js | 2 - .../js/manufacturing_overview.min.js.map | 1 - .../public/js/manufacturing_overview_desk.vue | 30 ++++----- .../public/js/production_plan.js | 31 +++++++++ 10 files changed, 158 insertions(+), 48 deletions(-) create mode 100644 manufacturing_overview/manufacturing_overview/productionplan.py delete mode 100644 manufacturing_overview/public/build.json create mode 100644 manufacturing_overview/public/js/manufacturing_overview.bundle.js delete mode 100644 manufacturing_overview/public/js/manufacturing_overview.min.js delete mode 100644 manufacturing_overview/public/js/manufacturing_overview.min.js.map create mode 100644 manufacturing_overview/public/js/production_plan.js diff --git a/manufacturing_overview/__init__.py b/manufacturing_overview/__init__.py index 7a0660b..c161d2f 100644 --- a/manufacturing_overview/__init__.py +++ b/manufacturing_overview/__init__.py @@ -1,3 +1,2 @@ -__version__ = '0.0.1' - +__version__ = '14.1.0' diff --git a/manufacturing_overview/hooks.py b/manufacturing_overview/hooks.py index 74b2b68..0ea2ed9 100644 --- a/manufacturing_overview/hooks.py +++ b/manufacturing_overview/hooks.py @@ -15,7 +15,7 @@ required_apps = ["erpnext"] # include js, css files in header of desk.html # app_include_css = "/assets/manufacturing_overview/css/manufacturing_overview.css" -app_include_js = "/assets/manufacturing_overview/js/manufacturing_overview.min.js" +app_include_js = "manufacturing_overview.bundle.js" # include js, css files in header of web template # web_include_css = "/assets/manufacturing_overview/css/manufacturing_overview.css" @@ -45,7 +45,7 @@ app_include_js = "/assets/manufacturing_overview/js/manufacturing_overview.min.j # website user home page (by Role) # role_home_page = { -# "Role": "home_page" +# "Role": "home_page" # } # Generators @@ -101,7 +101,7 @@ app_include_js = "/assets/manufacturing_overview/js/manufacturing_overview.min.j # "on_update": "method", # "on_cancel": "method", # "on_trash": "method" -# } +# } # } # Scheduled Tasks diff --git a/manufacturing_overview/manufacturing_overview/api.py b/manufacturing_overview/manufacturing_overview/api.py index 042d703..d1e5e71 100644 --- a/manufacturing_overview/manufacturing_overview/api.py +++ b/manufacturing_overview/manufacturing_overview/api.py @@ -2,9 +2,10 @@ import frappe from functools import reduce from datetime import date +# frappe.utils.logger.set_log_level("DEBUG") +# logger = frappe.logger("manufacturing_overview", +# allow_site=True, file_count=10000) -def clearCache(doc, event): - frappe.cache().delete_value("production_overview") def getDueInDays(d): delta = d - date.today() @@ -31,6 +32,7 @@ def shortenCustomerName(customer): else: return customer + def generateProductionOverviewCache(): salesOrderItems = frappe.db.sql( """ @@ -45,6 +47,7 @@ def generateProductionOverviewCache(): `tabSales Order Item`.item_code, `tabSales Order Item`.work_order_qty, `tabSales Order Item`.parent, + `tabSales Order Item`.production_plan_qty, `tabSales Order`.status as parentStatus FROM `tabSales Order Item` @@ -59,35 +62,59 @@ def generateProductionOverviewCache(): delivery_date, item_code, name """, as_dict=1) + currentWarehouseQtyList = [] + for soItem in salesOrderItems: + soItem.currentWarehouseQty = calculateCurrentWarehouseQty( + soItem.item_code, soItem.qty, currentWarehouseQtyList) + soItem.due_in = getDueInDays(soItem.delivery_date) soItem.customer = shortenCustomerName(frappe.get_value( 'Sales Order', soItem.parent, 'customer')) - # soItem.wos = frappe.get_all('Work Order', filters=[ - # ['sales_order', '=', soItem.parent], ['production_item', '=', soItem.item_code], ['status', '!=', "Cancelled"]]) - - soItem.delivery_date = frappe.utils.formatdate(soItem.delivery_date, 'dd.MM.yyyy') - soItem.warehouseamount = getAmountInWarehouses(soItem.item_code) - - if soItem.delivered_qty > 0 and soItem.delivered_qty < soItem.qty: - soItem.status = 'Partially Delivered' - elif soItem.delivered_qty >= soItem.qty: - soItem.status = 'Fully Delivered' - elif soItem.warehouseamount >= soItem.qty: - soItem.status = 'In Warehouse' - elif soItem.work_order_qty > 0: - soItem.status = 'To Produce' - else: - soItem.status = 'No Work Order' + soItem = formatDate(soItem) + soItem = calculateStatus(soItem) soItem.qty = soItem.qty - soItem.delivered_qty - soItem.link = '/app/sales-order/' + soItem.parent frappe.cache().set_value("production_overview", salesOrderItems) +def calculateCurrentWarehouseQty(item_code, qty, currentWarehouseQtyList): + for warehouseQty in currentWarehouseQtyList: + + if warehouseQty['item_code'] == item_code: + returnQty = warehouseQty['qty'] + warehouseQty['qty'] = warehouseQty['qty'] - qty + return returnQty + + fetchedWarehouseQty = getAmountInWarehouses(item_code) + currentWarehouseQtyList.append( + {'item_code': item_code, 'qty': fetchedWarehouseQty - qty}) + return fetchedWarehouseQty + + +def formatDate(item): + item.delivery_date = frappe.utils.formatdate( + item.delivery_date, 'dd.MM.yyyy') + return item + + +def calculateStatus(item): + if item.delivered_qty > 0 and item.delivered_qty < item.qty: + item.status = 'Partially Delivered' + elif item.delivered_qty >= item.qty: + item.status = 'Fully Delivered' + elif item.currentWarehouseQty >= item.qty: + item.status = 'In Warehouse' + elif item.work_order_qty > 0: + item.status = 'To Produce' + else: + item.status = 'No Work Order' + return item + + @frappe.whitelist() def getSalesorderOverviewList(): salesOrderItems = frappe.cache().get_value( diff --git a/manufacturing_overview/manufacturing_overview/productionplan.py b/manufacturing_overview/manufacturing_overview/productionplan.py new file mode 100644 index 0000000..de21ae0 --- /dev/null +++ b/manufacturing_overview/manufacturing_overview/productionplan.py @@ -0,0 +1,59 @@ +import frappe +from erpnext.stock.get_item_details import get_default_bom + +frappe.utils.logger.set_log_level("DEBUG") +logger = frappe.logger("manufacturing_overview", + allow_site=True, file_count=50) + +# logger.debug(f"{current_value} + {value} = {updated_value}") + + +@frappe.whitelist() +def makepp(sales_order): + """ + Check if Production Plan already exists and break when it does. + """ + so = frappe.get_doc("Sales Order", sales_order) + + checkpp = frappe.get_all('Production Plan Item', filters={'sales_order': so.name}, fields=[ + 'name', 'parent']) + + if checkpp: + return {'status': 400, + 'docname': [checkpp[0].parent]} + + """ + Create Production Plan + """ + sales_orders = [{ + "sales_order": so.name, + "customer": so.customer, + "sales_order_date": so.transaction_date, + "grand_total": so.grand_total, + }] + + po_items = [] + for item in so.items: + i = { + "item_code": item.item_code, + "bom_no": get_default_bom(item.item_code), + "planned_qty": item.qty, + "planned_start_date": frappe.utils.nowdate(), + "stock_uom": "Stk", + "sales_order": so.name, + "sales_order_item": item.name + } + + po_items.append(i) + + pp = frappe.get_doc({ + 'doctype': 'Production Plan', + 'get_items_from': 'Sales Order', + 'sales_orders': sales_orders, + 'po_items': po_items, + 'for_warehouse': "Lagerräume - HP" + }) + pp.insert() + + return {'status': 201, + 'docname': [pp.name]} diff --git a/manufacturing_overview/public/build.json b/manufacturing_overview/public/build.json deleted file mode 100644 index 18ffefe..0000000 --- a/manufacturing_overview/public/build.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "manufacturing_overview/js/manufacturing_overview.min.js": [ - "public/js/manufacturing_overview_page.js" - ] -} \ No newline at end of file diff --git a/manufacturing_overview/public/js/manufacturing_overview.bundle.js b/manufacturing_overview/public/js/manufacturing_overview.bundle.js new file mode 100644 index 0000000..91fcfad --- /dev/null +++ b/manufacturing_overview/public/js/manufacturing_overview.bundle.js @@ -0,0 +1,4 @@ +import './manufacturing_overview_desk.vue'; +import './manufacturing_overview_page.js'; +import './manufacturing_overview_row.vue'; +import './production_plan.js'; \ No newline at end of file diff --git a/manufacturing_overview/public/js/manufacturing_overview.min.js b/manufacturing_overview/public/js/manufacturing_overview.min.js deleted file mode 100644 index fc8e47c..0000000 --- a/manufacturing_overview/public/js/manufacturing_overview.min.js +++ /dev/null @@ -1,2 +0,0 @@ -!function(){"use strict";var e={name:"ManufacturingOverviewRow",props:["qty","item_name","item_code","customer","delivery_date","status","link","reference","due_in"],methods:{pushRoute:function(e){frappe.router.push_state(e)}}},n=function(){var e=this,n=e.$createElement,t=e._self._c||n;return t("a",{staticClass:"row link-item text-wrap ellipsis onbpoard-spotlight",attrs:{type:"Link"},on:{click:function(n){return e.pushRoute(e.link)}}},[t("div",{staticClass:"col col-xs-8"},[t("span",{staticClass:"indicator-pill no-margin",class:{red:"No Work Order"===e.status,blue:"Partially Delivered"===e.status,green:"In Warehouse"===e.status,yellow:"To Produce"===e.status,grey:"Unknown"===e.status}}),e._v(" "),t("span",{staticClass:"widget-subtitle"},[e._v(e._s(e.qty))]),e._v(" -\n "),t("span",{staticClass:"widget-title"},[e._v(e._s(e.item_name))]),e._v(" "),t("div",[e.customer&&e.item_code?t("small",{staticClass:"color-secondary"},[e._v(e._s(e.customer)+" -\n "),t("a",{attrs:{type:"Link"},on:{click:function(n){return e.pushRoute("/app#Form/Item/"+e.item_code)}}},[e._v(e._s(e.item_code))])]):e.customer?t("small",{staticClass:"color-secondary"},[e._v(e._s(e.customer))]):e.item_code?t("small",{staticClass:"color-secondary"},[e._v(e._s(e.item_code))]):e._e()]),e._v(" "),t("div",[t("small",{staticClass:"color-secondary"},[e._v(e._s(e.reference))])])]),e._v(" "),e.due_in<0?t("div",{staticClass:"text-muted ellipsis color-secondary col col-xs-4 text-right"},[t("b",{staticStyle:{color:"red"}},[e._v(e._s(e.delivery_date))])]):0===e.due_in?t("div",{staticClass:"text-muted ellipsis color-secondary col col-xs-4 text-right"},[t("b",{staticStyle:{color:"black"}},[e._v(e._s(e.delivery_date))])]):t("div",{staticClass:"text-muted ellipsis color-secondary col col-xs-4 text-right"},[e._v("\n "+e._s(e.delivery_date)+"\n ")])])};n._withStripped=!0;var t={components:{ManufacturingOverviewRow:function(e,n,t,i,s,a,r,o){var l,c=("function"==typeof t?t.options:t)||{};if(c.__file="/home/frappe/frappe-bench/apps/manufacturing_overview/manufacturing_overview/public/js/manufacturing_overview_row.vue",c.render||(c.render=e.render,c.staticRenderFns=e.staticRenderFns,c._compiled=!0,s&&(c.functional=!0)),c._scopeId=i,n&&(l=function(e){n.call(this,r(e))}),void 0!==l)if(c.functional){var d=c.render;c.render=function(e,n){return l.call(n),d(e,n)}}else{var u=c.beforeCreate;c.beforeCreate=u?[].concat(u,l):[l]}return c}({render:n,staticRenderFns:[]},function(e){e&&e("data-v-b81054e0_0",{source:"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",map:{version:3,sources:[],names:[],mappings:"",file:"manufacturing_overview_row.vue"},media:void 0})},e,void 0,!1,0,function e(){var n=document.head||document.getElementsByTagName("head")[0],t=e.styles||(e.styles={}),i="undefined"!=typeof navigator&&/msie [6-9]\\b/.test(navigator.userAgent.toLowerCase());return function(e,s){if(!document.querySelector('style[data-vue-ssr-id~="'+e+'"]')){var a=i?s.media||"default":e,r=t[a]||(t[a]={ids:[],parts:[],element:void 0});if(!r.ids.includes(e)){var o=s.source,l=r.ids.length;if(r.ids.push(e),i&&(r.element=r.element||document.querySelector("style[data-group="+a+"]")),!r.element){var c=r.element=document.createElement("style");c.type="text/css",s.media&&c.setAttribute("media",s.media),i&&(c.setAttribute("data-group",a),c.setAttribute("data-next-index","0")),n.appendChild(c)}if(i&&(l=parseInt(r.element.getAttribute("data-next-index")),r.element.setAttribute("data-next-index",l+1)),r.element.styleSheet)r.parts.push(o),r.element.styleSheet.cssText=r.parts.filter(Boolean).join("\n");else{var d=document.createTextNode(o),u=r.element.childNodes;u[l]&&r.element.removeChild(u[l]),u.length?r.element.insertBefore(d,u[l]):r.element.appendChild(d)}}}}})},data:function(){return{salesorderData:[{customer:"",delivery_date:"",link:"",name:"",item_name:"Loading...",item_code:"",qty:"",sales_order:"",status:"Unknown",due_in:0}],timer:"",origin:window.location.origin,userPermissions:{}}},created:function(){this.fetchEventsList(),this.timer=setInterval(this.fetchEventsList,3e4)},methods:{fetchEventsList:function(){var e=this;frappe.call({method:"manufacturing_overview.manufacturing_overview.api.getSalesorderOverviewList",async:!0,args:{},callback:function(n){n.message&&(e.salesorderData=n.message)}})},cancelAutoUpdate:function(){clearInterval(this.timer)},beforeDestroy:function(){clearInterval(this.timer)}}},i=function(){var e=this.$createElement,n=this._self._c||e;return n("div",{staticClass:"col-12 col-lg-4 layout-main-section-wrapper"},[n("div",{staticClass:"layout-main-section"},[n("div",{staticClass:"widget links-widget-box",staticStyle:{height:"auto"}},[this._m(0),this._v(" "),n("div",{staticClass:"widget-body"},this._l(this.salesorderData,function(e){return n("manufacturing-overview-row",{key:e.name,attrs:{qty:e.qty,item_name:e.item_name,item_code:e.item_code,customer:e.customer,delivery_date:e.delivery_date,status:e.status,link:e.link,reference:e.parent,due_in:e.due_in}})}),1)])])])};i._withStripped=!0;var s=function(e,n,t,i,s,a,r,o){var l,c=("function"==typeof t?t.options:t)||{};if(c.__file="/home/frappe/frappe-bench/apps/manufacturing_overview/manufacturing_overview/public/js/manufacturing_overview_desk.vue",c.render||(c.render=e.render,c.staticRenderFns=e.staticRenderFns,c._compiled=!0,s&&(c.functional=!0)),c._scopeId=i,n&&(l=function(e){n.call(this,r(e))}),void 0!==l)if(c.functional){var d=c.render;c.render=function(e,n){return l.call(n),d(e,n)}}else{var u=c.beforeCreate;c.beforeCreate=u?[].concat(u,l):[l]}return c}({render:i,staticRenderFns:[function(){var e=this.$createElement,n=this._self._c||e;return n("div",{staticClass:"widget-head"},[n("div",[n("div",{staticClass:"widget-subtitle"})]),this._v(" "),n("div",{staticClass:"widget-control"})])}]},function(e){e&&e("data-v-0f8b43ee_0",{source:"\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n",map:{version:3,sources:[],names:[],mappings:"",file:"manufacturing_overview_desk.vue"},media:void 0})},t,void 0,!1,0,function e(){var n=document.head||document.getElementsByTagName("head")[0],t=e.styles||(e.styles={}),i="undefined"!=typeof navigator&&/msie [6-9]\\b/.test(navigator.userAgent.toLowerCase());return function(e,s){if(!document.querySelector('style[data-vue-ssr-id~="'+e+'"]')){var a=i?s.media||"default":e,r=t[a]||(t[a]={ids:[],parts:[],element:void 0});if(!r.ids.includes(e)){var o=s.source,l=r.ids.length;if(r.ids.push(e),i&&(r.element=r.element||document.querySelector("style[data-group="+a+"]")),!r.element){var c=r.element=document.createElement("style");c.type="text/css",s.media&&c.setAttribute("media",s.media),i&&(c.setAttribute("data-group",a),c.setAttribute("data-next-index","0")),n.appendChild(c)}if(i&&(l=parseInt(r.element.getAttribute("data-next-index")),r.element.setAttribute("data-next-index",l+1)),r.element.styleSheet)r.parts.push(o),r.element.styleSheet.cssText=r.parts.filter(Boolean).join("\n");else{var d=document.createTextNode(o),u=r.element.childNodes;u[l]&&r.element.removeChild(u[l]),u.length?r.element.insertBefore(d,u[l]):r.element.appendChild(d)}}}}});$(document).ready(function(){$(".layout-main-section-wrapper").after('
');new Vue({el:"#manufacturing-overview-body",render:function(e){return e(s,{})}})})}(); -//# sourceMappingURL=manufacturing_overview.min.js.map diff --git a/manufacturing_overview/public/js/manufacturing_overview.min.js.map b/manufacturing_overview/public/js/manufacturing_overview.min.js.map deleted file mode 100644 index b75e3b4..0000000 --- a/manufacturing_overview/public/js/manufacturing_overview.min.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"manufacturing_overview.min.js","sources":["../../../../apps/manufacturing_overview/manufacturing_overview/public/js/manufacturing_overview_row.vue?rollup-plugin-vue=script.js","../../../../apps/manufacturing_overview/manufacturing_overview/public/js/manufacturing_overview_desk.vue?rollup-plugin-vue=script.js","../../../../apps/manufacturing_overview/manufacturing_overview/public/js/manufacturing_overview_page.js"],"sourcesContent":["//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n\nexport default {\n name: \"ManufacturingOverviewRow\",\n props: [\n \"qty\",\n \"item_name\",\n \"item_code\",\n \"customer\",\n \"delivery_date\",\n \"status\",\n \"link\",\n \"reference\",\n \"due_in\",\n ],\n methods: {\n pushRoute(link) {\n frappe.router.push_state(link);\n },\n },\n};\n","//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n//\n\nimport ManufacturingOverviewRow from \"./manufacturing_overview_row.vue\";\n\nexport default {\n components: {\n ManufacturingOverviewRow,\n },\n data() {\n return {\n salesorderData: [\n {\n customer: \"\",\n delivery_date: \"\",\n link: \"\",\n name: \"\",\n item_name: \"Loading...\",\n item_code: \"\",\n qty: \"\",\n sales_order: \"\",\n status: \"Unknown\",\n due_in: 0,\n },\n ],\n timer: \"\",\n origin: window.location.origin,\n userPermissions: {},\n };\n },\n created() {\n this.fetchEventsList();\n this.timer = setInterval(this.fetchEventsList, 30000);\n },\n methods: {\n fetchEventsList() {\n let self = this;\n frappe.call({\n method:\n \"manufacturing_overview.manufacturing_overview.api.getSalesorderOverviewList\",\n async: true,\n args: {},\n callback: function (r) {\n if (r.message) {\n self.salesorderData = r.message;\n }\n },\n });\n },\n cancelAutoUpdate() {\n clearInterval(this.timer);\n },\n beforeDestroy() {\n clearInterval(this.timer);\n },\n },\n};\n","import ManufacturingOverviewDesk from \"./manufacturing_overview_desk.vue\";\n\n$(document).ready(function () {\n $(\".layout-main-section-wrapper\").after('
');\n var pod = new Vue({\n el: \"#manufacturing-overview-body\",\n render(h) {\n return h(ManufacturingOverviewDesk, {});\n }\n });\n});"],"names":["name","props","methods","pushRoute","link","frappe","router","push_state","components","data","salesorderData","customer","delivery_date","item_name","item_code","qty","sales_order","status","due_in","timer","origin","window","location","userPermissions","created","this","fetchEventsList","setInterval","let","self","call","method","async","args","callback","r","message","cancelAutoUpdate","clearInterval","beforeDestroy","$","document","ready","after","Vue","el","render","h","ManufacturingOverviewDesk"],"mappings":"+BA4De,CACbA,KAAM,2BACNC,MAAO,CACL,MACA,YACA,YACA,WACA,gBACA,SACA,OACA,YACA,UAEFC,QAAS,CACPC,mBAAUC,GACRC,OAAOC,OAAOC,WAAWH,0mDCzChB,CACbI,WAAY,o8DAGZC,gBACE,MAAO,CACLC,eAAgB,CACd,CACEC,SAAU,GACVC,cAAe,GACfR,KAAM,GACNJ,KAAM,GACNa,UAAW,aACXC,UAAW,GACXC,IAAK,GACLC,YAAa,GACbC,OAAQ,UACRC,OAAQ,IAGZC,MAAO,GACPC,OAAQC,OAAOC,SAASF,OACxBG,gBAAiB,KAGrBC,mBACEC,KAAKC,kBACLD,KAAKN,MAAQQ,YAAYF,KAAKC,gBAAiB,MAEjDxB,QAAS,CACPwB,2BACEE,IAAIC,EAAOJ,KACXpB,OAAOyB,KAAK,CACVC,OACE,8EACFC,OAAO,EACPC,KAAM,GACNC,SAAU,SAAUC,GACdA,EAAEC,UACJP,EAAKnB,eAAiByB,EAAEC,aAKhCC,4BACEC,cAAcb,KAAKN,QAErBoB,yBACED,cAAcb,KAAKN,8vFChFzBqB,EAAEC,UAAUC,MAAM,WACdF,EAAE,gCAAgCG,MAAM,oGAC9B,IAAIC,IAAI,CACdC,GAAI,+BACJC,gBAAOC,GACH,OAAOA,EAAEC,EAA2B"} \ No newline at end of file diff --git a/manufacturing_overview/public/js/manufacturing_overview_desk.vue b/manufacturing_overview/public/js/manufacturing_overview_desk.vue index 9b9df97..ce7ceda 100644 --- a/manufacturing_overview/public/js/manufacturing_overview_desk.vue +++ b/manufacturing_overview/public/js/manufacturing_overview_desk.vue @@ -3,25 +3,24 @@