From 8be0f0ddb71b7ec9ac168588e6e8d5a84aa88a95 Mon Sep 17 00:00:00 2001 From: Paul Brinkmeier Date: Sun, 20 Aug 2023 18:26:39 +0200 Subject: [PATCH] Implement price calculator --- frontend/Entry.elm | 123 +++++++++++++++++- jon/static/entry.js | 307 ++++++++++++++++++++++++++++++++++++++++---- jon/static/jon.css | 3 + 3 files changed, 402 insertions(+), 31 deletions(-) diff --git a/frontend/Entry.elm b/frontend/Entry.elm index c4c91a5..e0a40fe 100644 --- a/frontend/Entry.elm +++ b/frontend/Entry.elm @@ -90,6 +90,35 @@ type alias Globals = , taxGroups : List TaxGroup } +type CalculatorTax = Net | Gross + +ctToInt ct = case ct of + Gross -> 0 + Net -> 1 + +ctShow ct = case ct of + Gross -> "Brutto" + Net -> "Netto" + +type alias CalculatorModel = + { tax : Select.Model CalculatorTax + , bundlePrice : NumberInput.Model Float + , bundleSize : NumberInput.Model Int + } + +type CalculatorMsg + = SetTax String + | SetBundlePrice String + | SetBundleSize String + +updateCalculator msg model = case msg of + SetTax key -> + { model | tax = Select.update key model.tax } + SetBundlePrice str -> + { model | bundlePrice = NumberInput.update str model.bundlePrice } + SetBundleSize str -> + { model | bundleSize = NumberInput.update str model.bundleSize } + type State = ItemSearch { searchTerm : String @@ -99,6 +128,7 @@ type State { barcode : String , name : String , salesUnits : NumberInput.Model Int + , calculator : CalculatorModel , netUnitPrice : NumberInput.Model Float , grossUnitPrice : NumberInput.Model Float , group : Select.Model Group @@ -114,6 +144,7 @@ type Msg | SetBarcode String | SetName String | SetSalesUnits String + | CalculatorMsg CalculatorMsg | SetNetUnitPrice String | SetGrossUnitPrice String | SetGroup String @@ -148,6 +179,11 @@ updateState msg globals state = case state of ( ItemEditor { barcode = searchResult.barcode , name = searchResult.name + , calculator = + { tax = Select.init ctShow ctShow Net [Net, Gross] + , bundlePrice = NumberInput.fromFloat searchResult.netUnitPrice + , bundleSize = NumberInput.fromInt 1 + } , netUnitPrice = NumberInput.fromFloat searchResult.netUnitPrice , grossUnitPrice = NumberInput.fromFloat (suggestedGrossPrice searchResult.netUnitPrice taxGroup.percentage) @@ -167,6 +203,8 @@ updateState msg globals state = case state of (ItemEditor { model | name = name }, Cmd.none) SetSalesUnits str -> (ItemEditor { model | salesUnits = NumberInput.update str model.salesUnits }, Cmd.none) + CalculatorMsg msg_ -> + (ItemEditor { model | calculator = updateCalculator msg_ model.calculator }, Cmd.none) SetNetUnitPrice str -> ( ItemEditor { model | netUnitPrice = NumberInput.update str model.netUnitPrice } , Cmd.none @@ -227,6 +265,11 @@ view { globals, state } = case state of [ label [ for "location" ] [ text "Raum" ] , Select.view SetLocation model.location ] + , div [ class "form-input" ] + [ label [ for "tax-group" ] [ text "Steuergruppe" ] + , Select.view SetTaxGroup model.taxGroup + ] + , Html.map CalculatorMsg <| viewCalculator model.calculator (Select.get model.taxGroup) , div [ class "form-input" ] [ label [ for "net-unit-price" ] [ text "Einkaufspreis (Netto)" ] , input @@ -237,10 +280,7 @@ view { globals, state } = case state of , step "0.01" ] [] - ] - , div [ class "form-input" ] - [ label [ for "tax-group" ] [ text "Steuergruppe" ] - , Select.view SetTaxGroup model.taxGroup + , viewSetCalculatedPriceButton model ] , div [ class "form-input" ] [ label [ for "gross-unit-price" ] [ text "Verkaufspreis (Brutto)" ] @@ -257,6 +297,81 @@ view { globals, state } = case state of ] ] +viewCalculator model taxGroup = + let + mainPart = + [ text "(" + , input + [ class "formula-input", placeholder "Gebindepreis" + , value <| NumberInput.show model.bundlePrice + , onInput SetBundlePrice + ] + [] + , text " ÷ " + , input + [ class "formula-input", placeholder "Gebindegröße" + , value <| NumberInput.show model.bundleSize + , onInput SetBundleSize + ] + [] + , text ") " + ] + taxPart = + [ text " ÷ " + , input + [ class "formula-input" + , disabled True + , value <| String.fromFloat <| 1 + taxGroup.percentage + ] + [] + ] + resultPart = + [ text " = " + , input + [ class "formula-input" + , disabled True + , value <| Maybe.withDefault "?" <| Maybe.map String.fromFloat <| calculatorResult model taxGroup + ] + [] + ] + in + fieldset [] + [ legend [] [ text "Preisrechner" ] + , div [] <| List.concat <| List.filterMap identity + [ Just mainPart + , if Select.get model.tax == Gross then Just taxPart else Nothing + , Just resultPart + ] + , div [ class "form-input" ] + [ label [ for "calculator-tax" ] [ text "Gebindepreis ist" ] + , Select.view SetTax model.tax + ] + ] + +calculatorResult model taxGroup = + case (NumberInput.get model.bundlePrice, NumberInput.get model.bundleSize) of + (Just bundlePrice, Just bundleSize) -> + Just <| roundTo 2 <| + if Select.get model.tax == Gross then + (bundlePrice / toFloat bundleSize) / (1 + taxGroup.percentage) + else + bundlePrice / toFloat bundleSize + _ -> + Nothing + +viewSetCalculatedPriceButton model = + case calculatorResult model.calculator (Select.get model.taxGroup) of + Nothing -> + button [ disabled True ] [ text "Auf ? setzen" ] + Just calculatedPrice -> + button + [ onClick <| SetNetUnitPrice <| String.fromFloat calculatedPrice + -- Prevent submitting the form + , type_ "button" + ] + [ text <| "Auf " ++ String.fromFloat calculatedPrice ++ " setzen" + ] + viewSetSuggestedPriceButton model = case NumberInput.get model.netUnitPrice of Nothing -> diff --git a/jon/static/entry.js b/jon/static/entry.js index fd6e49f..f185616 100644 --- a/jon/static/entry.js +++ b/jon/static/entry.js @@ -10739,12 +10739,21 @@ var $elm$core$Basics$never = function (_v0) { var $elm$browser$Browser$element = _Browser_element; var $elm$core$Platform$Sub$batch = _Platform_batch; var $elm$core$Platform$Sub$none = $elm$core$Platform$Sub$batch(_List_Nil); +var $author$project$Entry$Gross = {$: 'Gross'}; var $author$project$Entry$ItemEditor = function (a) { return {$: 'ItemEditor', a: a}; }; +var $author$project$Entry$Net = {$: 'Net'}; var $author$project$Entry$ReceiveSearchResults = function (a) { return {$: 'ReceiveSearchResults', a: a}; }; +var $author$project$Entry$ctShow = function (ct) { + if (ct.$ === 'Gross') { + return 'Brutto'; + } else { + return 'Netto'; + } +}; var $elm$http$Http$BadStatus_ = F2( function (a, b) { return {$: 'BadStatus_', a: a, b: b}; @@ -11163,6 +11172,32 @@ var $author$project$Select$update = F2( {selected: x}); } }); +var $author$project$Entry$updateCalculator = F2( + function (msg, model) { + switch (msg.$) { + case 'SetTax': + var key = msg.a; + return _Utils_update( + model, + { + tax: A2($author$project$Select$update, key, model.tax) + }); + case 'SetBundlePrice': + var str = msg.a; + return _Utils_update( + model, + { + bundlePrice: A2($author$project$NumberInput$update, str, model.bundlePrice) + }); + default: + var str = msg.a; + return _Utils_update( + model, + { + bundleSize: A2($author$project$NumberInput$update, str, model.bundleSize) + }); + } + }); var $author$project$Entry$updateState = F3( function (msg, globals, state) { if (state.$ === 'ItemSearch') { @@ -11217,6 +11252,17 @@ var $author$project$Entry$updateState = F3( $author$project$Entry$ItemEditor( { barcode: searchResult.barcode, + calculator: { + bundlePrice: $author$project$NumberInput$fromFloat(searchResult.netUnitPrice), + bundleSize: $author$project$NumberInput$fromInt(1), + tax: A4( + $author$project$Select$init, + $author$project$Entry$ctShow, + $author$project$Entry$ctShow, + $author$project$Entry$Net, + _List_fromArray( + [$author$project$Entry$Net, $author$project$Entry$Gross])) + }, grossUnitPrice: $author$project$NumberInput$fromFloat( A2($author$project$Entry$suggestedGrossPrice, searchResult.netUnitPrice, taxGroup.percentage)), group: A4( @@ -11298,6 +11344,16 @@ var $author$project$Entry$updateState = F3( salesUnits: A2($author$project$NumberInput$update, str, model.salesUnits) })), $elm$core$Platform$Cmd$none); + case 'CalculatorMsg': + var msg_ = msg.a; + return _Utils_Tuple2( + $author$project$Entry$ItemEditor( + _Utils_update( + model, + { + calculator: A2($author$project$Entry$updateCalculator, msg_, model.calculator) + })), + $elm$core$Platform$Cmd$none); case 'SetNetUnitPrice': var str = msg.a; return _Utils_Tuple2( @@ -11364,6 +11420,9 @@ var $author$project$Entry$update = F2( {globals: globals, state: state_}, cmd); }); +var $author$project$Entry$CalculatorMsg = function (a) { + return {$: 'CalculatorMsg', a: a}; +}; var $author$project$Entry$SetBarcode = function (a) { return {$: 'SetBarcode', a: a}; }; @@ -11404,6 +11463,9 @@ var $elm$html$Html$Attributes$disabled = $elm$html$Html$Attributes$boolProperty( var $elm$html$Html$fieldset = _VirtualDom_node('fieldset'); var $elm$html$Html$Attributes$for = $elm$html$Html$Attributes$stringProperty('htmlFor'); var $elm$html$Html$form = _VirtualDom_node('form'); +var $author$project$Select$get = function ($) { + return $.selected; +}; var $elm$html$Html$label = _VirtualDom_node('label'); var $elm$html$Html$legend = _VirtualDom_node('legend'); var $elm$html$Html$Events$alwaysPreventDefault = function (msg) { @@ -11531,6 +11593,161 @@ var $author$project$Select$view = F2( ]), A2($elm$core$List$map, viewOption, model.options)); }); +var $author$project$Entry$SetBundlePrice = function (a) { + return {$: 'SetBundlePrice', a: a}; +}; +var $author$project$Entry$SetBundleSize = function (a) { + return {$: 'SetBundleSize', a: a}; +}; +var $author$project$Entry$SetTax = function (a) { + return {$: 'SetTax', a: a}; +}; +var $author$project$NumberInput$get = function ($) { + return $.value; +}; +var $author$project$Entry$calculatorResult = F2( + function (model, taxGroup) { + var _v0 = _Utils_Tuple2( + $author$project$NumberInput$get(model.bundlePrice), + $author$project$NumberInput$get(model.bundleSize)); + if ((_v0.a.$ === 'Just') && (_v0.b.$ === 'Just')) { + var bundlePrice = _v0.a.a; + var bundleSize = _v0.b.a; + return $elm$core$Maybe$Just( + A2( + $author$project$Entry$roundTo, + 2, + _Utils_eq( + $author$project$Select$get(model.tax), + $author$project$Entry$Gross) ? ((bundlePrice / bundleSize) / (1 + taxGroup.percentage)) : (bundlePrice / bundleSize))); + } else { + return $elm$core$Maybe$Nothing; + } + }); +var $elm$core$Maybe$map = F2( + function (f, maybe) { + if (maybe.$ === 'Just') { + var value = maybe.a; + return $elm$core$Maybe$Just( + f(value)); + } else { + return $elm$core$Maybe$Nothing; + } + }); +var $elm$html$Html$Attributes$placeholder = $elm$html$Html$Attributes$stringProperty('placeholder'); +var $author$project$Entry$viewCalculator = F2( + function (model, taxGroup) { + var taxPart = _List_fromArray( + [ + $elm$html$Html$text(' ÷ '), + A2( + $elm$html$Html$input, + _List_fromArray( + [ + $elm$html$Html$Attributes$class('formula-input'), + $elm$html$Html$Attributes$disabled(true), + $elm$html$Html$Attributes$value( + $elm$core$String$fromFloat(1 + taxGroup.percentage)) + ]), + _List_Nil) + ]); + var resultPart = _List_fromArray( + [ + $elm$html$Html$text(' = '), + A2( + $elm$html$Html$input, + _List_fromArray( + [ + $elm$html$Html$Attributes$class('formula-input'), + $elm$html$Html$Attributes$disabled(true), + $elm$html$Html$Attributes$value( + A2( + $elm$core$Maybe$withDefault, + '?', + A2( + $elm$core$Maybe$map, + $elm$core$String$fromFloat, + A2($author$project$Entry$calculatorResult, model, taxGroup)))) + ]), + _List_Nil) + ]); + var mainPart = _List_fromArray( + [ + $elm$html$Html$text('('), + A2( + $elm$html$Html$input, + _List_fromArray( + [ + $elm$html$Html$Attributes$class('formula-input'), + $elm$html$Html$Attributes$placeholder('Gebindepreis'), + $elm$html$Html$Attributes$value( + $author$project$NumberInput$show(model.bundlePrice)), + $elm$html$Html$Events$onInput($author$project$Entry$SetBundlePrice) + ]), + _List_Nil), + $elm$html$Html$text(' ÷ '), + A2( + $elm$html$Html$input, + _List_fromArray( + [ + $elm$html$Html$Attributes$class('formula-input'), + $elm$html$Html$Attributes$placeholder('Gebindegröße'), + $elm$html$Html$Attributes$value( + $author$project$NumberInput$show(model.bundleSize)), + $elm$html$Html$Events$onInput($author$project$Entry$SetBundleSize) + ]), + _List_Nil), + $elm$html$Html$text(') ') + ]); + return A2( + $elm$html$Html$fieldset, + _List_Nil, + _List_fromArray( + [ + A2( + $elm$html$Html$legend, + _List_Nil, + _List_fromArray( + [ + $elm$html$Html$text('Preisrechner') + ])), + A2( + $elm$html$Html$div, + _List_Nil, + $elm$core$List$concat( + A2( + $elm$core$List$filterMap, + $elm$core$Basics$identity, + _List_fromArray( + [ + $elm$core$Maybe$Just(mainPart), + _Utils_eq( + $author$project$Select$get(model.tax), + $author$project$Entry$Gross) ? $elm$core$Maybe$Just(taxPart) : $elm$core$Maybe$Nothing, + $elm$core$Maybe$Just(resultPart) + ])))), + A2( + $elm$html$Html$div, + _List_fromArray( + [ + $elm$html$Html$Attributes$class('form-input') + ]), + _List_fromArray( + [ + A2( + $elm$html$Html$label, + _List_fromArray( + [ + $elm$html$Html$Attributes$for('calculator-tax') + ]), + _List_fromArray( + [ + $elm$html$Html$text('Gebindepreis ist') + ])), + A2($author$project$Select$view, $author$project$Entry$SetTax, model.tax) + ])) + ])); + }); var $author$project$Entry$GotoItemEditor = function (a) { return {$: 'GotoItemEditor', a: a}; }; @@ -11632,11 +11849,39 @@ var $author$project$Entry$viewSearchResult = function (model) { ])) ])); }; -var $author$project$NumberInput$get = function ($) { - return $.value; -}; -var $author$project$Select$get = function ($) { - return $.selected; +var $author$project$Entry$viewSetCalculatedPriceButton = function (model) { + var _v0 = A2( + $author$project$Entry$calculatorResult, + model.calculator, + $author$project$Select$get(model.taxGroup)); + if (_v0.$ === 'Nothing') { + return A2( + $elm$html$Html$button, + _List_fromArray( + [ + $elm$html$Html$Attributes$disabled(true) + ]), + _List_fromArray( + [ + $elm$html$Html$text('Auf ? setzen') + ])); + } else { + var calculatedPrice = _v0.a; + return A2( + $elm$html$Html$button, + _List_fromArray( + [ + $elm$html$Html$Events$onClick( + $author$project$Entry$SetNetUnitPrice( + $elm$core$String$fromFloat(calculatedPrice))), + $elm$html$Html$Attributes$type_('button') + ]), + _List_fromArray( + [ + $elm$html$Html$text( + 'Auf ' + ($elm$core$String$fromFloat(calculatedPrice) + ' setzen')) + ])); + } }; var $author$project$Entry$viewSetSuggestedPriceButton = function (model) { var _v0 = $author$project$NumberInput$get(model.netUnitPrice); @@ -11884,6 +12129,33 @@ var $author$project$Entry$view = function (_v0) { ])), A2( $elm$html$Html$div, + _List_fromArray( + [ + $elm$html$Html$Attributes$class('form-input') + ]), + _List_fromArray( + [ + A2( + $elm$html$Html$label, + _List_fromArray( + [ + $elm$html$Html$Attributes$for('tax-group') + ]), + _List_fromArray( + [ + $elm$html$Html$text('Steuergruppe') + ])), + A2($author$project$Select$view, $author$project$Entry$SetTaxGroup, model.taxGroup) + ])), + A2( + $elm$html$Html$map, + $author$project$Entry$CalculatorMsg, + A2( + $author$project$Entry$viewCalculator, + model.calculator, + $author$project$Select$get(model.taxGroup))), + A2( + $elm$html$Html$div, _List_fromArray( [ $elm$html$Html$Attributes$class('form-input') @@ -11911,27 +12183,8 @@ var $author$project$Entry$view = function (_v0) { $elm$html$Html$Attributes$id('net-unit-price'), $elm$html$Html$Attributes$step('0.01') ]), - _List_Nil) - ])), - A2( - $elm$html$Html$div, - _List_fromArray( - [ - $elm$html$Html$Attributes$class('form-input') - ]), - _List_fromArray( - [ - A2( - $elm$html$Html$label, - _List_fromArray( - [ - $elm$html$Html$Attributes$for('tax-group') - ]), - _List_fromArray( - [ - $elm$html$Html$text('Steuergruppe') - ])), - A2($author$project$Select$view, $author$project$Entry$SetTaxGroup, model.taxGroup) + _List_Nil), + $author$project$Entry$viewSetCalculatedPriceButton(model) ])), A2( $elm$html$Html$div, @@ -12053,4 +12306,4 @@ _Platform_export({'Entry':{'init':$author$project$Entry$main( }, A2($elm$json$Json$Decode$field, 'id', $elm$json$Json$Decode$int)); }, - A2($elm$json$Json$Decode$field, 'percentage', $elm$json$Json$Decode$float))))))({"versions":{"elm":"0.19.1"},"types":{"message":"Entry.Msg","aliases":{"Entry.SearchResult":{"args":[],"type":"{ barcode : String.String, name : String.String, netUnitPrice : Basics.Float, bought : String.String, salesUnits : Basics.Int, available : Basics.Bool, locationName : String.String, locationId : Basics.Int, groupName : String.String, groupId : Basics.Int, taxGroupId : Basics.Int }"}},"unions":{"Entry.Msg":{"args":[],"tags":{"SetSearchTerm":["String.String"],"SubmitSearch":[],"ReceiveSearchResults":["Result.Result Http.Error (List.List Entry.SearchResult)"],"GotoItemEditor":["Entry.SearchResult"],"SetBarcode":["String.String"],"SetName":["String.String"],"SetSalesUnits":["String.String"],"SetNetUnitPrice":["String.String"],"SetGrossUnitPrice":["String.String"],"SetGroup":["String.String"],"SetLocation":["String.String"],"SetTaxGroup":["String.String"]}},"Basics.Bool":{"args":[],"tags":{"True":[],"False":[]}},"Http.Error":{"args":[],"tags":{"BadUrl":["String.String"],"Timeout":[],"NetworkError":[],"BadStatus":["Basics.Int"],"BadBody":["String.String"]}},"Basics.Float":{"args":[],"tags":{"Float":[]}},"Basics.Int":{"args":[],"tags":{"Int":[]}},"List.List":{"args":["a"],"tags":{}},"Result.Result":{"args":["error","value"],"tags":{"Ok":["value"],"Err":["error"]}},"String.String":{"args":[],"tags":{"String":[]}}}}})}});}(this)); \ No newline at end of file + A2($elm$json$Json$Decode$field, 'percentage', $elm$json$Json$Decode$float))))))({"versions":{"elm":"0.19.1"},"types":{"message":"Entry.Msg","aliases":{"Entry.SearchResult":{"args":[],"type":"{ barcode : String.String, name : String.String, netUnitPrice : Basics.Float, bought : String.String, salesUnits : Basics.Int, available : Basics.Bool, locationName : String.String, locationId : Basics.Int, groupName : String.String, groupId : Basics.Int, taxGroupId : Basics.Int }"}},"unions":{"Entry.Msg":{"args":[],"tags":{"SetSearchTerm":["String.String"],"SubmitSearch":[],"ReceiveSearchResults":["Result.Result Http.Error (List.List Entry.SearchResult)"],"GotoItemEditor":["Entry.SearchResult"],"SetBarcode":["String.String"],"SetName":["String.String"],"SetSalesUnits":["String.String"],"CalculatorMsg":["Entry.CalculatorMsg"],"SetNetUnitPrice":["String.String"],"SetGrossUnitPrice":["String.String"],"SetGroup":["String.String"],"SetLocation":["String.String"],"SetTaxGroup":["String.String"]}},"Basics.Bool":{"args":[],"tags":{"True":[],"False":[]}},"Entry.CalculatorMsg":{"args":[],"tags":{"SetTax":["String.String"],"SetBundlePrice":["String.String"],"SetBundleSize":["String.String"]}},"Http.Error":{"args":[],"tags":{"BadUrl":["String.String"],"Timeout":[],"NetworkError":[],"BadStatus":["Basics.Int"],"BadBody":["String.String"]}},"Basics.Float":{"args":[],"tags":{"Float":[]}},"Basics.Int":{"args":[],"tags":{"Int":[]}},"List.List":{"args":["a"],"tags":{}},"Result.Result":{"args":["error","value"],"tags":{"Ok":["value"],"Err":["error"]}},"String.String":{"args":[],"tags":{"String":[]}}}}})}});}(this)); \ No newline at end of file diff --git a/jon/static/jon.css b/jon/static/jon.css index 9897996..386c1fd 100644 --- a/jon/static/jon.css +++ b/jon/static/jon.css @@ -63,3 +63,6 @@ th { .form-input > select { display: block; } +.formula-input { + width: 10em; +}