gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-merchant-backoffice] branch master updated: product stock managem


From: gnunet
Subject: [taler-merchant-backoffice] branch master updated: product stock management
Date: Wed, 21 Apr 2021 00:15:10 +0200

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a commit to branch master
in repository merchant-backoffice.

The following commit(s) were added to refs/heads/master by this push:
     new e9482a5  product stock management
e9482a5 is described below

commit e9482a5c90ee6cfbe647b50520716ed5ea46a944
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Tue Apr 20 19:14:57 2021 -0300

    product stock management
---
 CHANGELOG.md                                       |   5 -
 README.md                                          |   1 -
 packages/frontend/src/components/form/Field.tsx    |  12 +-
 packages/frontend/src/components/form/Input.tsx    |  22 +--
 .../frontend/src/components/form/InputArray.tsx    |  10 +-
 .../frontend/src/components/form/InputBoolean.tsx  |  10 +-
 .../frontend/src/components/form/InputCurrency.tsx |  11 +-
 .../frontend/src/components/form/InputDate.tsx     |  48 ++++--
 .../frontend/src/components/form/InputDuration.tsx |   4 +-
 .../frontend/src/components/form/InputGroup.tsx    |   2 +-
 .../frontend/src/components/form/InputImage.tsx    |  26 +++-
 .../form/{InputCurrency.tsx => InputNumber.tsx}    |  14 +-
 .../frontend/src/components/form/InputSecured.tsx  |  10 +-
 .../frontend/src/components/form/InputSelector.tsx |  12 +-
 .../src/components/form/InputStock.stories.tsx     | 110 +++++++++++++
 .../frontend/src/components/form/InputStock.tsx    | 171 +++++++++++++++++++++
 .../frontend/src/components/form/InputTaxes.tsx    |  93 +++++++++++
 .../src/components/form/InputWithAddon.tsx         |  14 +-
 .../src/components/product/ProductForm.tsx         |  70 +++++----
 packages/frontend/src/hooks/order.ts               |   8 +-
 packages/frontend/src/hooks/product.ts             |  13 +-
 packages/frontend/src/messages/en.po               | 125 ++++++++++++---
 .../frontend/src/paths/admin/create/CreatePage.tsx |   5 +-
 packages/frontend/src/paths/admin/list/Table.tsx   |   1 -
 .../paths/instance/orders/create/CreatePage.tsx    |  22 +--
 .../orders/create/InventoryProductForm.tsx         |   8 +-
 .../orders/create/NonInventoryProductForm.tsx      |   3 +-
 .../paths/instance/orders/details/DetailPage.tsx   |  11 +-
 .../src/paths/instance/orders/list/Table.tsx       |   3 +-
 .../paths/instance/products/create/CreatePage.tsx  |   4 +-
 .../products/create/CreatedSuccessfully.tsx        |   6 +-
 .../src/paths/instance/products/create/index.tsx   |   2 +-
 .../src/paths/instance/products/list/Table.tsx     |  82 ++++++----
 .../src/paths/instance/products/list/index.tsx     |   7 -
 .../paths/instance/products/update/UpdatePage.tsx  |   4 +-
 .../src/paths/instance/update/UpdatePage.tsx       |   5 +-
 packages/frontend/src/schemas/index.ts             |  28 ++--
 37 files changed, 760 insertions(+), 222 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 51e0f45..5d7b60d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,20 +13,15 @@ and this project adheres to [Semantic 
Versioning](https://semver.org/spec/v2.0.0
  - validate everything using onChange
  - feature: input as date format
 
- - implement better error handling (improve creation of duplicated instances)
  - replace Yup and type definition with a taler-library for the purpose (first 
wait Florian to refactor wallet core)
  - add more doc style comments 
  - configure eslint
  - configure prettier
  - prune scss styles to reduce size
- - some way to copy the url of a created instance
  - fix mobile: some things are still on the left
  - edit button to go to instance settings
  - check if there is a way to remove auto async for /routes 
/components/{async,routes} so it can be turned on when building 
non-single-bundle
- 
  - product detail: we could have some button that brings us to the detailed 
screen for the product
- - input number
-
  - navigation to another instance should not do full refresh
  - cleanup instance and token management, because code is a mess and can be 
refactored 
 ## [Unreleased]
diff --git a/README.md b/README.md
index fd031e1..13fbeb6 100644
--- a/README.md
+++ b/README.md
@@ -100,4 +100,3 @@ Result will be placed at 
`packages/frontend/single/index.html`
 *   Date-fns: library for manipulating javascript date
 
 *   messageformat: ICU MessageFormat for Javascript, with support for .po and 
.mo files 
-
diff --git a/packages/frontend/src/components/form/Field.tsx 
b/packages/frontend/src/components/form/Field.tsx
index 53ef1e6..8d643cf 100644
--- a/packages/frontend/src/components/form/Field.tsx
+++ b/packages/frontend/src/components/form/Field.tsx
@@ -27,6 +27,7 @@ export interface FormType<T> {
   initial: Partial<T>;
   errors: FormErrors<T>;
   toStr: FormtoStr<T>;
+  name: string;
   fromStr: FormfromStr<T>;
   valueHandler: StateUpdater<Partial<T>>;
 }
@@ -57,15 +58,17 @@ export type FormUpdater<T> = {
 interface ProviderProps<T> {
   object?: Partial<T>;
   errors?: FormErrors<T>;
+  name?: string;
   valueHandler: StateUpdater<Partial<T>>;
   children: ComponentChildren
 }
 
-export function FormProvider<T>({ object = {}, errors = {}, valueHandler, 
children }: ProviderProps<T>) {
+export function FormProvider<T>({ object = {}, errors = {}, name = '', 
valueHandler, children }: ProviderProps<T>) {
   const initial = useMemo(() => object,[])
-  const value = useMemo<FormType<T>>(() => ({errors, object, initial, 
valueHandler, toStr: {}, fromStr: {}}), [errors, object, valueHandler])
+  const value = useMemo<FormType<T>>(() => ({errors, object, initial, 
valueHandler, name, toStr: {}, fromStr: {}}), [errors, object, valueHandler])
+
   return <FormContext.Provider value={value}>
-    <form onSubmit={(e) => {
+    <form class="field" onSubmit={(e) => {
       e.preventDefault()
       valueHandler(object)
     }}>
@@ -92,7 +95,7 @@ const setValueDeeper = (object: any, names: string[], value: 
any): any => {
 }
 
 export function useField<T>(name: keyof T) {
-  const { errors, object, initial, toStr, fromStr, valueHandler } = 
useContext<FormType<T>>(FormContext)
+  const { errors, object, initial, name: formName, toStr, fromStr, 
valueHandler } = useContext<FormType<T>>(FormContext)
   type P = typeof name
   type V = T[P]
 
@@ -108,6 +111,7 @@ export function useField<T>(name: keyof T) {
   return {
     error: errors[name],
     value: readField(object, String(name)),
+    formName,
     initial: initial[name],
     onChange: updateField(name),
     toStr: toStr[name] ? toStr[name]! : defaultToString,
diff --git a/packages/frontend/src/components/form/Input.tsx 
b/packages/frontend/src/components/form/Input.tsx
index e7af633..8b7cbc8 100644
--- a/packages/frontend/src/components/form/Input.tsx
+++ b/packages/frontend/src/components/form/Input.tsx
@@ -30,26 +30,27 @@ interface Props<T> {
   toStr?: (v?: any) => string;
   fromStr?: (s: string) => any;
   inputExtra?: any,
+  side?: ComponentChildren;
   children?: ComponentChildren;
 }
 
 const defaultToString = (f?: any): string => f || ''
 const defaultFromString = (v: string): any => v as any
 
-const TextInput = ({inputType, error, ...rest}:any) => inputType === 
'multiline' ? 
-  <textarea {...rest} class={error ? "textarea is-danger" : "textarea"} 
rows="3"  /> : 
+const TextInput = ({ inputType, error, ...rest }: any) => inputType === 
'multiline' ?
+  <textarea {...rest} class={error ? "textarea is-danger" : "textarea"} 
rows="3" /> :
   <input {...rest} class={error ? "input is-danger" : "input"} 
type={inputType} />;
 
-export function Input<T>({ name, readonly, expand, children, inputType, 
inputExtra, fromStr = defaultFromString, toStr = defaultToString }: Props<keyof 
T>): VNode {
-  const { error, value, onChange } = useField<T>(name);
+export function Input<T>({ name, readonly, expand, children, inputType, 
inputExtra, side, fromStr = defaultFromString, toStr = defaultToString }: 
Props<keyof T>): VNode {
+  const { error, value, onChange, formName } = useField<T>(name);
 
-  const placeholder = useMessage(`fields.instance.${name}.placeholder`);
-  const tooltip = useMessage(`fields.instance.${name}.tooltip`);
+  const placeholder = useMessage(`fields.${!formName ? 'instance' : 
formName}.${name}.placeholder`);
+  const tooltip = useMessage(`fields.${!formName ? 'instance' : 
formName}.${name}.tooltip`);
 
   return <div class="field is-horizontal">
     <div class="field-label is-normal">
       <label class="label">
-        <Message id={`fields.instance.${name}.label`} />
+        <Message id={`fields.${!formName ? 'instance' : 
formName}.${name}.label`} />
         {tooltip && <span class="icon" data-tooltip={tooltip}>
           <i class="mdi mdi-information" />
         </span>}
@@ -61,15 +62,16 @@ export function Input<T>({ name, readonly, expand, 
children, inputType, inputExt
           <TextInput error={error} {...inputExtra}
             inputType={inputType}
             placeholder={placeholder} readonly={readonly}
-            name={String(name)} value={toStr(value)} 
-            onChange={(e:h.JSX.TargetedEvent<HTMLInputElement>): void => 
onChange(fromStr(e.currentTarget.value))} />
-          <Message id={`fields.instance.${name}.help`}> </Message>
+            name={String(name)} value={toStr(value)}
+            onChange={(e: h.JSX.TargetedEvent<HTMLInputElement>): void => 
onChange(fromStr(e.currentTarget.value))} />
+          <Message id={`fields.${!formName ? 'instance' : 
formName}.${name}.help`}> </Message>
           {children}
         </p>
         {error ? <p class="help is-danger">
           <Message id={`validation.${error.type}`} 
fields={error.params}>{error.message} </Message>
         </p> : null}
       </div>
+      {side}
     </div>
   </div>;
 }
diff --git a/packages/frontend/src/components/form/InputArray.tsx 
b/packages/frontend/src/components/form/InputArray.tsx
index 80feec8..f09faa3 100644
--- a/packages/frontend/src/components/form/InputArray.tsx
+++ b/packages/frontend/src/components/form/InputArray.tsx
@@ -36,10 +36,10 @@ const defaultToString = (f?: any): string => f || ''
 const defaultFromString = (v: string): any => v as any
 
 export function InputArray<T>({ name, readonly, addonBefore, isValid = () => 
true, fromStr = defaultFromString, toStr = defaultToString }: Props<keyof T>): 
VNode {
-  const { error: formError, value, onChange } = useField<T>(name);
+  const { error: formError, value, onChange, formName } = useField<T>(name);
   const [localError, setLocalError] = useState<ValidationError | null>(null)
-  const placeholder = useMessage(`fields.instance.${name}.placeholder`);
-  const tooltip = useMessage(`fields.instance.${name}.tooltip`);
+  const placeholder = useMessage(`fields.${!formName ? 'instance' : 
formName}.${name}.placeholder`);
+  const tooltip = useMessage(`fields.${!formName ? 'instance' : 
formName}.${name}.tooltip`);
 
   const error = formError || localError
 
@@ -50,7 +50,7 @@ export function InputArray<T>({ name, readonly, addonBefore, 
isValid = () => tru
   return <div class="field is-horizontal">
     <div class="field-label is-normal">
       <label class="label">
-        <Message id={`fields.instance.${name}.label`} />
+        <Message id={`fields.${!formName ? 'instance' : 
formName}.${name}.label`} />
         {tooltip && <span class="icon" data-tooltip={tooltip}>
           <i class="mdi mdi-information" />
         </span>}
@@ -67,7 +67,7 @@ export function InputArray<T>({ name, readonly, addonBefore, 
isValid = () => tru
               placeholder={placeholder} readonly={readonly} disabled={readonly}
               name={String(name)} value={currentValue}
               onChange={(e): void => setCurrentValue(e.currentTarget.value)} />
-            <Message id={`fields.instance.${name}.help`}> </Message>
+            <Message id={`fields.${!formName ? 'instance' : 
formName}.${name}.help`}> </Message>
           </p>
           <p class="control">
             <button class="button is-info" onClick={(): void => {
diff --git a/packages/frontend/src/components/form/InputBoolean.tsx 
b/packages/frontend/src/components/form/InputBoolean.tsx
index 3a4e36b..0bdbb7b 100644
--- a/packages/frontend/src/components/form/InputBoolean.tsx
+++ b/packages/frontend/src/components/form/InputBoolean.tsx
@@ -37,10 +37,10 @@ const defaultFromBoolean = (v: boolean | undefined): any => 
v as any
 
 
 export function InputBoolean<T>({ name, readonly, threeState, expand, 
fromBoolean = defaultFromBoolean, toBoolean = defaultToBoolean }: Props<keyof 
T>): VNode {
-  const { error, value, onChange } = useField<T>(name);
+  const { error, value, onChange, formName } = useField<T>(name);
 
-  const placeholder = useMessage(`fields.instance.${name}.placeholder`);
-  const tooltip = useMessage(`fields.instance.${name}.tooltip`);
+  const placeholder = useMessage(`fields.${!formName ? 'instance' : 
formName}.${name}.placeholder`);
+  const tooltip = useMessage(`fields.${!formName ? 'instance' : 
formName}.${name}.tooltip`);
 
   const onCheckboxClick = (): void => {
     const c = toBoolean(value)
@@ -51,7 +51,7 @@ export function InputBoolean<T>({ name, readonly, threeState, 
expand, fromBoolea
   return <div class="field is-horizontal">
     <div class="field-label is-normal">
       <label class="label">
-        <Message id={`fields.instance.${name}.label`} />
+        <Message id={`fields.${!formName ? 'instance' : 
formName}.${name}.label`} />
         {tooltip && <span class="icon has-tooltip-right" 
data-tooltip={tooltip}>
           <i class="mdi mdi-information" />
         </span>}
@@ -69,7 +69,7 @@ export function InputBoolean<T>({ name, readonly, threeState, 
expand, fromBoolea
 
             <span class="check" />
           </label>
-          <Message id={`fields.instance.${name}.help`}> </Message>
+          <Message id={`fields.${!formName ? 'instance' : 
formName}.${name}.help`}> </Message>
         </p>
         {error ? <p class="help is-danger">
           <Message id={`validation.${error.type}`} 
fields={error.params}>{error.message} </Message>
diff --git a/packages/frontend/src/components/form/InputCurrency.tsx 
b/packages/frontend/src/components/form/InputCurrency.tsx
index f932d34..18f0b2b 100644
--- a/packages/frontend/src/components/form/InputCurrency.tsx
+++ b/packages/frontend/src/components/form/InputCurrency.tsx
@@ -19,6 +19,7 @@
 * @author Sebastian Javier Marchano (sebasjm)
 */
 import { ComponentChildren, h } from "preact";
+import { useConfigContext } from "../../context/backend";
 import { Amount } from "../../declaration";
 import { InputWithAddon } from "./InputWithAddon";
 
@@ -26,17 +27,19 @@ export interface Props<T> {
   name: keyof T;
   readonly?: boolean;
   expand?: boolean;
-  currency: string;
   addonAfter?: ComponentChildren;
   children?: ComponentChildren;
+  side?: ComponentChildren;
 }
 
-export function InputCurrency<T>({ name, readonly, expand, currency, 
addonAfter, children }: Props<T>) {
-  return <InputWithAddon<T> name={name} readonly={readonly} 
addonBefore={currency}
+export function InputCurrency<T>({ name, readonly, expand, addonAfter, 
children, side }: Props<T>) {
+  const config = useConfigContext()
+  return <InputWithAddon<T> name={name} readonly={readonly} 
addonBefore={config.currency}
+  side={side}
     addonAfter={addonAfter}
     inputType='number' expand={expand}
     toStr={(v?: Amount) => v?.split(':')[1] || ''}
-    fromStr={(v: string) => !v ? '' : `${currency}:${v}`}
+    fromStr={(v: string) => !v ? '' : `${config.currency}:${v}`}
     inputExtra={{ min: 0 }}
     children={children}
   />
diff --git a/packages/frontend/src/components/form/InputDate.tsx 
b/packages/frontend/src/components/form/InputDate.tsx
index ce077e7..a6ee828 100644
--- a/packages/frontend/src/components/form/InputDate.tsx
+++ b/packages/frontend/src/components/form/InputDate.tsx
@@ -31,19 +31,34 @@ export interface Props<T> {
   name: keyof T;
   readonly?: boolean;
   expand?: boolean;
+  //FIXME: create separated components InputDate and InputTimestamp
+  withTimestampSupport?: boolean;
 }
 
-export function InputDate<T>({ name, readonly, expand }: Props<T>) {
+export function InputDate<T>({ name, readonly, expand, withTimestampSupport }: 
Props<T>) {
   const [opened, setOpened] = useState(false)
-  const { error, value, onChange } = useField<T>(name);
+  const [editing, setEditing] = useState(false)
 
-  const placeholder = useMessage(`fields.instance.${name}.placeholder`);
-  const tooltip = useMessage(`fields.instance.${name}.tooltip`);
+  const { error, value, onChange, formName } = useField<T>(name);
+
+  const placeholder = useMessage(`fields.${!formName ? 'instance' : 
formName}.${name}.placeholder`);
+  const tooltip = useMessage(`fields.${!formName ? 'instance' : 
formName}.${name}.tooltip`);
+
+  let strValue = ''
+  if (!value) {
+    strValue = withTimestampSupport ? 'unknown' : ''
+  } else if (value instanceof Date) {
+    strValue = format(value, 'yyyy/MM/dd HH:mm:ss')
+  } else if (value.t_ms) {
+    strValue = value.t_ms === 'never' ?
+      (withTimestampSupport ? 'never' : '') :
+      format(new Date(value.t_ms), 'yyyy/MM/dd HH:mm:ss')
+  }
 
   return <div class="field is-horizontal">
     <div class="field-label is-normal">
       <label class="label">
-        <Message id={`fields.instance.${name}.label`} />
+        <Message id={`fields.${!formName ? 'instance' : 
formName}.${name}.label`} />
         {tooltip && <span class="icon" data-tooltip={tooltip}>
           <i class="mdi mdi-information" />
         </span>}
@@ -53,12 +68,12 @@ export function InputDate<T>({ name, readonly, expand }: 
Props<T>) {
       <div class="field">
         <div class="field has-addons">
           <p class={expand ? "control is-expanded" : "control"}>
-            <input class="input" type="text" 
-              readonly value={!value ? '' : format(value, 'yyyy/MM/dd 
HH:mm:ss')} 
-              placeholder="pick a date" 
+            <input class="input" type="text"
+              readonly value={strValue}
+              placeholder="pick a date"
               onClick={() => setOpened(true)}
-              />
-            <Message id={`fields.instance.${name}.help`}> </Message>
+            />
+            <Message id={`fields.${!formName ? 'instance' : 
formName}.${name}.help`}> </Message>
           </p>
           <div class="control" onClick={() => setOpened(true)}>
             <a class="button is-static" >
@@ -68,11 +83,22 @@ export function InputDate<T>({ name, readonly, expand }: 
Props<T>) {
         </div>
         {error ? <p class="help is-danger"><Message 
id={`validation.${error.type}`} 
fields={error.params}>{error.message}</Message></p> : null}
       </div>
+
+      <button class="button is-info mr-3" onClick={() => onChange(undefined as 
any)} >clear</button>
+      {withTimestampSupport &&
+        <button class="button is-info" onClick={() => onChange({ t_ms: 'never' 
} as any)}>never</button>
+      }
     </div>
     <DatePicker
       opened={opened}
       closeFunction={() => setOpened(false)}
-      dateReceiver={(d) => onChange(d as any)}
+      dateReceiver={(d) => {
+        if (withTimestampSupport) {
+          onChange({t_ms: d.getTime()} as any)
+        } else {
+          onChange(d as any)
+        }
+      }}
     />
   </div>;
 }
diff --git a/packages/frontend/src/components/form/InputDuration.tsx 
b/packages/frontend/src/components/form/InputDuration.tsx
index 18b55a3..aa349ce 100644
--- a/packages/frontend/src/components/form/InputDuration.tsx
+++ b/packages/frontend/src/components/form/InputDuration.tsx
@@ -34,8 +34,8 @@ export function InputDuration<T>({ name, expand, readonly }: 
Props<T>) {
   const { value } = useField<T>(name);
   return <InputWithAddon<T> name={name} readonly={readonly} 
addonAfter={readableDuration(value as any)}
     expand={expand}
-    toStr={(v?: RelativeTime) => `${(v && v.d_ms !== "forever" && v.d_ms ? 
v.d_ms / 1000 : '')}`}
-    fromStr={(v: string) => ({ d_ms: (parseInt(v, 10) * 1000) || undefined })}
+    toStr={(v?: RelativeTime) => `${(v && v.d_ms !== "forever" && v.d_ms ? 
v.d_ms : '')}`}
+    fromStr={(v: string) => ({ d_ms: (parseInt(v, 10)) || undefined })}
   />
 }
 
diff --git a/packages/frontend/src/components/form/InputGroup.tsx 
b/packages/frontend/src/components/form/InputGroup.tsx
index e80ef66..3208285 100644
--- a/packages/frontend/src/components/form/InputGroup.tsx
+++ b/packages/frontend/src/components/form/InputGroup.tsx
@@ -37,7 +37,7 @@ export function InputGroup<T>({ name, description, children, 
alternative}: Props
   return <div class="card">
     <header class="card-header">
       <p class={ !group?.hasError ? "card-header-title" : "card-header-title 
has-text-danger"}>
-        { description ? description : <Message 
id={`fields.instance.${String(name)}.label`} /> }
+        { description ? description : <Message 
id={`fields.groups.${String(name)}.label`} /> }
       </p>
       <button class="card-header-icon" aria-label="more options" onClick={(): 
void => setActive(!active)}>
         <span class="icon">
diff --git a/packages/frontend/src/components/form/InputImage.tsx 
b/packages/frontend/src/components/form/InputImage.tsx
index 8227f3b..153cf3d 100644
--- a/packages/frontend/src/components/form/InputImage.tsx
+++ b/packages/frontend/src/components/form/InputImage.tsx
@@ -22,7 +22,7 @@ import { ComponentChildren, Fragment, h } from "preact";
 import { useField } from "./Field";
 import emptyImage from "../../assets/empty.png";
 import { Message, useMessage } from "preact-messages";
-import { useRef } from "preact/hooks";
+import { useRef, useState } from "preact/hooks";
 
 export interface Props<T> {
   name: keyof T;
@@ -33,16 +33,18 @@ export interface Props<T> {
 }
 
 export function InputImage<T>({ name, readonly, children, expand }: Props<T>) {
-  const { error, value, onChange } = useField<T>(name);
+  const { error, value, onChange, formName } = useField<T>(name);
 
-  const placeholder = useMessage(`fields.instance.${name}.placeholder`);
-  const tooltip = useMessage(`fields.instance.${name}.tooltip`);
+  const placeholder = useMessage(`fields.${!formName ? 'instance' : 
formName}.${name}.placeholder`);
+  const tooltip = useMessage(`fields.${!formName ? 'instance' : 
formName}.${name}.tooltip`);
   const image = useRef<HTMLInputElement>(null)
 
+  const [sizeError, setSizeError] = useState(false)
+
   return <div class="field is-horizontal">
     <div class="field-label is-normal">
       <label class="label">
-        <Message id={`fields.instance.${name}.label`} />
+        <Message id={`fields.${!formName ? 'instance' : 
formName}.${name}.label`} />
         {tooltip && <span class="icon" data-tooltip={tooltip}>
           <i class="mdi mdi-information" />
         </span>}
@@ -58,7 +60,14 @@ export function InputImage<T>({ name, readonly, children, 
expand }: Props<T>) {
             placeholder={placeholder} readonly={readonly}
             onChange={e => {
               const f: FileList | null = e.currentTarget.files
-              if (!f || f.length != 1 || f[0].size > 10000000) return 
onChange(emptyImage)
+              if (!f || f.length != 1) {
+                return onChange(emptyImage)
+              }
+              if (f[0].size > 1024*1024) {
+                setSizeError(true)
+                return onChange(emptyImage)
+              }
+              setSizeError(false)
               f[0].arrayBuffer().then(b => {
                 const b64 = btoa(
                   new Uint8Array(b)
@@ -67,12 +76,15 @@ export function InputImage<T>({ name, readonly, children, 
expand }: Props<T>) {
                 onChange(`data:${f[0].type};base64,${b64}` as any)
               })
             }} />
-          <Message id={`fields.instance.${name}.help`}> </Message>
+          <Message id={`fields.${!formName ? 'instance' : 
formName}.${name}.help`}> </Message>
           {children}
         </p>
         {error ? <p class="help is-danger">
           <Message id={`validation.${error.type}`} 
fields={error.params}>{error.message} </Message>
         </p> : null}
+        {sizeError ? <p class="help is-danger">
+          <Message id={`validation.imageSizeLimit`} />
+        </p> : null}
       </div>
     </div>
   </div>
diff --git a/packages/frontend/src/components/form/InputCurrency.tsx 
b/packages/frontend/src/components/form/InputNumber.tsx
similarity index 74%
copy from packages/frontend/src/components/form/InputCurrency.tsx
copy to packages/frontend/src/components/form/InputNumber.tsx
index f932d34..6362f11 100644
--- a/packages/frontend/src/components/form/InputCurrency.tsx
+++ b/packages/frontend/src/components/form/InputNumber.tsx
@@ -19,26 +19,26 @@
 * @author Sebastian Javier Marchano (sebasjm)
 */
 import { ComponentChildren, h } from "preact";
+import { useConfigContext } from "../../context/backend";
 import { Amount } from "../../declaration";
+import { Input } from "./Input";
 import { InputWithAddon } from "./InputWithAddon";
 
 export interface Props<T> {
   name: keyof T;
   readonly?: boolean;
   expand?: boolean;
-  currency: string;
-  addonAfter?: ComponentChildren;
+  side?: ComponentChildren;
   children?: ComponentChildren;
 }
 
-export function InputCurrency<T>({ name, readonly, expand, currency, 
addonAfter, children }: Props<T>) {
-  return <InputWithAddon<T> name={name} readonly={readonly} 
addonBefore={currency}
-    addonAfter={addonAfter}
+export function InputNumber<T>({ name, readonly, expand, children, side }: 
Props<T>) {
+  return <InputWithAddon<T> name={name} readonly={readonly} 
+    fromStr={(v) => parseInt(v, 10)} toStr={(v) => ""+v}
     inputType='number' expand={expand}
-    toStr={(v?: Amount) => v?.split(':')[1] || ''}
-    fromStr={(v: string) => !v ? '' : `${currency}:${v}`}
     inputExtra={{ min: 0 }}
     children={children}
+    side={side}
   />
 }
 
diff --git a/packages/frontend/src/components/form/InputSecured.tsx 
b/packages/frontend/src/components/form/InputSecured.tsx
index b29ea4c..d5e36c7 100644
--- a/packages/frontend/src/components/form/InputSecured.tsx
+++ b/packages/frontend/src/components/form/InputSecured.tsx
@@ -39,10 +39,10 @@ const TokenStatus = ({ prev, post }: any) => {
 }
 
 export function InputSecured<T>({ name, readonly }: Props<T>): VNode {
-  const { error, value, initial, onChange, toStr, fromStr } = 
useField<T>(name);
+  const { error, value, initial, onChange, toStr, fromStr, formName } = 
useField<T>(name);
 
-  const placeholder = useMessage(`fields.instance.${name}.placeholder`, {});
-  const tooltip = useMessage(`fields.instance.${name}.tooltip`, {});
+  const placeholder = useMessage(`fields.${!formName ? 'instance' : 
formName}.${name}.placeholder`, {});
+  const tooltip = useMessage(`fields.${!formName ? 'instance' : 
formName}.${name}.tooltip`, {});
 
   const [active, setActive] = useState(false);
   const [newValue, setNuewValue] = useState("")
@@ -51,7 +51,7 @@ export function InputSecured<T>({ name, readonly }: 
Props<T>): VNode {
     <div class="field is-horizontal">
       <div class="field-label is-normal">
         <label class="label">
-          <Message id={`fields.instance.${name}.label`} />
+          <Message id={`fields.${!formName ? 'instance' : 
formName}.${name}.label`} />
           {tooltip && <span class="icon" data-tooltip={tooltip}>
             <i class="mdi mdi-information" />
           </span>}
@@ -81,7 +81,7 @@ export function InputSecured<T>({ name, readonly }: 
Props<T>): VNode {
                   onInput={(e): void => {
                     setNuewValue(e.currentTarget.value)
                   }} />
-                <Message id={`fields.instance.${name}.help`}> </Message>
+                <Message id={`fields.${!formName ? 'instance' : 
formName}.${name}.help`}> </Message>
               </div>
               <div class="control">
                 <button class="button is-info" disabled={fromStr(newValue) === 
value} onClick={(): void => { onChange(fromStr(newValue)); setActive(!active); 
setNuewValue(""); }} >
diff --git a/packages/frontend/src/components/form/InputSelector.tsx 
b/packages/frontend/src/components/form/InputSelector.tsx
index 48ec0a6..36ea728 100644
--- a/packages/frontend/src/components/form/InputSelector.tsx
+++ b/packages/frontend/src/components/form/InputSelector.tsx
@@ -35,15 +35,15 @@ const defaultToString = (f?: any): string => f || ''
 const defaultFromString = (v: string): any => v as any
 
 export function InputSelector<T>({ name, readonly, expand, values, fromStr = 
defaultFromString, toStr = defaultToString }: Props<keyof T>): VNode {
-  const { error, value, onChange } = useField<T>(name);
+  const { error, value, onChange, formName } = useField<T>(name);
 
-  const placeholder = useMessage(`fields.instance.${name}.placeholder`);
-  const tooltip = useMessage(`fields.instance.${name}.tooltip`);
+  const placeholder = useMessage(`fields.${!formName ? 'instance' : 
formName}.${name}.placeholder`);
+  const tooltip = useMessage(`fields.${!formName ? 'instance' : 
formName}.${name}.tooltip`);
 
   return <div class="field is-horizontal">
     <div class="field-label is-normal">
       <label class="label">
-        <Message id={`fields.instance.${name}.label`} />
+        <Message id={`fields.${!formName ? 'instance' : 
formName}.${name}.label`} />
         {tooltip && <span class="icon" data-tooltip={tooltip}>
           <i class="mdi mdi-information" />
         </span>}
@@ -53,13 +53,13 @@ export function InputSelector<T>({ name, readonly, expand, 
values, fromStr = def
       <div class="field">
         <p class={expand ? "control is-expanded select" : "control select"}>
           <select class={error ? "select is-danger" : "select"}
-            name={String(name)}  disabled={readonly} readonly={readonly}
+            name={String(name)} disabled={readonly} readonly={readonly}
             onChange={(e) => { onChange(fromStr(e.currentTarget.value)) }}>
             <option>{placeholder}</option>
             {values
               .map(v => <option value={toStr(v)}>{toStr(v)}</option>)}
           </select>
-          <Message id={`fields.instance.${name}.help`}> </Message>
+          <Message id={`fields.${!formName ? 'instance' : 
formName}.${name}.help`}> </Message>
         </p>
         {error ? <p class="help is-danger">
           <Message id={`validation.${error.type}`} 
fields={error.params}>{error.message} </Message>
diff --git a/packages/frontend/src/components/form/InputStock.stories.tsx 
b/packages/frontend/src/components/form/InputStock.stories.tsx
new file mode 100644
index 0000000..5fae9dc
--- /dev/null
+++ b/packages/frontend/src/components/form/InputStock.stories.tsx
@@ -0,0 +1,110 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+
+import { addDays } from 'date-fns';
+import { h, VNode } from 'preact';
+import { useState } from 'preact/hooks';
+import { FormProvider } from './Field';
+import { InputStock, Stock } from './InputStock'
+
+export default {
+  title: 'Fields/InputStock',
+  component: InputStock,
+};
+
+type T = { stock?: Stock }
+
+export const CreateStockEmpty = () => {
+  const [state, setState] = useState<Partial<T>>({})
+  return <FormProvider<T> name="product" object={state} errors={{}} 
valueHandler={setState}>
+    <InputStock<T> name="stock"  />
+    <div><pre>{JSON.stringify(state, undefined, 2)}</pre></div>
+  </FormProvider>
+}
+
+export const CreateStockUnknownRestock = () => {
+  const [state, setState] = useState<Partial<T>>({
+    stock: {
+      current: 10,
+      lost: 0,
+      sold: 0,
+    }
+  })
+  return <FormProvider<T> name="product" object={state} errors={{}} 
valueHandler={setState}>
+    <InputStock<T> name="stock"  />
+    <div><pre>{JSON.stringify(state, undefined, 2)}</pre></div>
+  </FormProvider>
+}
+
+export const CreateStockNoRestock = () => {
+  const [state, setState] = useState<Partial<T>>({
+    stock: {
+      current: 10,
+      lost: 0,
+      sold: 0,
+      nextRestock: { t_ms: 'never' }
+    }
+  })
+  return <FormProvider<T> name="product" object={state} errors={{}} 
valueHandler={setState}>
+    <InputStock<T> name="stock"  />
+    <div><pre>{JSON.stringify(state, undefined, 2)}</pre></div>
+  </FormProvider>
+}
+
+export const CreateStockWithRestock = () => {
+  const [state, setState] = useState<Partial<T>>({
+    stock: {
+      current: 15,
+      lost: 0,
+      sold: 0,
+      nextRestock: { t_ms: addDays(new Date(), 1).getTime() }
+    }
+  })
+  return <FormProvider<T> name="product" object={state} errors={{}} 
valueHandler={setState}>
+    <InputStock<T> name="stock"  />
+    <div><pre>{JSON.stringify(state, undefined, 2)}</pre></div>
+  </FormProvider>
+}
+
+export const UpdatingProductWithManagedStock = () => {
+  const [state, setState] = useState<Partial<T>>({
+    stock: {
+      current: 100,
+      lost: 0,
+      sold: 0,
+      nextRestock: { t_ms: addDays(new Date(), 1).getTime() }
+    }
+  })
+  return <FormProvider<T> name="product" object={state} errors={{}} 
valueHandler={setState}>
+    <InputStock<T> name="stock" alreadyExist />
+    <div><pre>{JSON.stringify(state, undefined, 2)}</pre></div>
+  </FormProvider>
+}
+
+export const UpdatingProductWithInfiniteStock = () => {
+  const [state, setState] = useState<Partial<T>>({})
+  return <FormProvider<T> name="product" object={state} errors={{}} 
valueHandler={setState}>
+    <InputStock<T> name="stock" alreadyExist />
+    <div><pre>{JSON.stringify(state, undefined, 2)}</pre></div>
+  </FormProvider>
+}
+
+
diff --git a/packages/frontend/src/components/form/InputStock.tsx 
b/packages/frontend/src/components/form/InputStock.tsx
new file mode 100644
index 0000000..22690b1
--- /dev/null
+++ b/packages/frontend/src/components/form/InputStock.tsx
@@ -0,0 +1,171 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+import { Fragment, h } from "preact";
+import { MerchantBackend, Timestamp } from "../../declaration";
+import { FormErrors, FormProvider, useField } from "./Field";
+import { useLayoutEffect, useState } from "preact/hooks";
+import { Input } from "./Input";
+import { Message, useMessage } from "preact-messages";
+import { InputGroup } from "./InputGroup";
+import { InputNumber } from "./InputNumber";
+import { InputDate } from "./InputDate";
+
+export interface Props<T> {
+  name: keyof T;
+  readonly?: boolean;
+  alreadyExist?: boolean;
+}
+
+
+type Entity = Stock
+
+export interface Stock {
+  current: number;
+  lost: number;
+  sold: number;
+  address?: MerchantBackend.Location;
+  nextRestock?: Timestamp;
+}
+
+interface StockDelta {
+  incoming: number;
+  lost: number;
+}
+
+
+export function InputStock<T>({ name, readonly, alreadyExist }: Props<T>) {
+  const { error, value, onChange, formName } = useField<T>(name);
+
+  const [errors, setErrors] = useState<FormErrors<Entity>>({})
+
+  const tooltip = useMessage(`fields.${!formName ? 'instance' : 
formName}.${name}.tooltip`, {});
+
+  const [formValue, valueHandler] = useState<Partial<Entity>>(value)
+  const [addedStock, setAddedStock] = useState<StockDelta>({ incoming: 0, 
lost: 0 })
+
+  useLayoutEffect(() => {
+    console.log(formValue)
+
+    onChange({
+      ...formValue,
+      current: (formValue?.current || 0) + addedStock.incoming,
+      lost: (formValue?.lost || 0) + addedStock.lost
+    } as any)
+  }, [formValue, addedStock])
+
+  if (!formValue) {
+    return <Fragment>
+      <div class="field is-horizontal">
+        <div class="field-label is-normal">
+          <label class="label">
+            <Message id={`fields.${!formName ? 'instance' : 
formName}.${name}.label`} />
+            {tooltip && <span class="icon" data-tooltip={tooltip}>
+              <i class="mdi mdi-information" />
+            </span>}
+          </label>
+        </div>
+        <div class="field-body is-flex-grow-3">
+          <div class="field has-addons">
+            {!alreadyExist ?
+              <button class="button" onClick={(): void => { valueHandler({ 
current: 0, lost: 0, sold: 0 } as Stock as any); }} >
+                <span>Manage stock</span>
+              </button> : <button class="button" disabled >
+                <span>Infinite</span>
+              </button>
+            }
+          </div>
+        </div>
+      </div>
+    </Fragment >
+  }
+
+  const currentStock = (formValue.current || 0) - (formValue.lost || 0) - 
(formValue.sold || 0)
+
+  const stockAddedErrors: FormErrors<typeof addedStock> = {
+    lost: currentStock + addedStock.incoming < addedStock.lost ? {
+      message: `lost cannot be greater that current + incoming (max 
${currentStock + addedStock.incoming})`
+    } : undefined
+  }
+
+  const stockUpdateDescription = stockAddedErrors.lost ? '' : (
+    !!addedStock.incoming || !!addedStock.lost ?
+      `current stock will change from ${currentStock} to ${currentStock + 
addedStock.incoming - addedStock.lost}` :
+      `current stock will stay at ${currentStock}`
+  )
+
+  return <Fragment>
+    <div class="card">
+      <header class="card-header">
+        <p class="card-header-title">Stock</p>
+      </header>
+      <div class="card-content">
+        <FormProvider<Entity> name="stock" errors={errors} object={formValue} 
valueHandler={valueHandler}>
+          {alreadyExist ? <Fragment>
+
+            <FormProvider name="added" errors={stockAddedErrors} 
object={addedStock} valueHandler={setAddedStock as any}>
+              <InputNumber name="incoming" />
+              <InputNumber name="lost" />
+            </FormProvider>
+
+            <div class="field is-horizontal">
+              <div class="field-label is-normal"></div>
+              <div class="field-body is-flex-grow-3">
+                <div class="field">
+                  {stockUpdateDescription}
+                </div>
+              </div>
+            </div>
+          </Fragment> : <InputNumber<Entity> name="current"
+            side={
+              <button class="button is-danger" onClick={(): void => { 
valueHandler(undefined as any) }} >
+                <span>without stock</span>
+              </button>
+            }
+          />}
+
+          <InputDate<Entity> name="nextRestock" withTimestampSupport />
+
+          <InputGroup<Entity> name="address">
+
+            <Input name="address.country" />
+
+            <Input name="address.address_lines" inputType="multiline"
+              toStr={(v: string[] | undefined) => !v ? '' : v.join('\n')}
+              fromStr={(v: string) => v.split('\n')}
+            />
+
+            <Input name="address.building_number" />
+            <Input name="address.building_name" />
+            <Input name="address.street" />
+            <Input name="address.post_code" />
+            <Input name="address.town_location" />
+            <Input name="address.town" />
+            <Input name="address.district" />
+            <Input name="address.country_subdivision" />
+          </InputGroup>
+        </FormProvider>
+      </div>
+    </div>
+  </Fragment>
+}
+  // (
+
+
diff --git a/packages/frontend/src/components/form/InputTaxes.tsx 
b/packages/frontend/src/components/form/InputTaxes.tsx
new file mode 100644
index 0000000..666c16e
--- /dev/null
+++ b/packages/frontend/src/components/form/InputTaxes.tsx
@@ -0,0 +1,93 @@
+/*
+ This file is part of GNU Taler
+ (C) 2021 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+/**
+*
+* @author Sebastian Javier Marchano (sebasjm)
+*/
+import { h } from "preact";
+import { InputArray } from "./InputArray";
+import { AMOUNT_REGEX, PAYTO_REGEX } from "../../utils/constants";
+import { useConfigContext } from "../../context/backend";
+import { Amount, MerchantBackend } from "../../declaration";
+import { FormErrors, FormProvider, useField } from "./Field";
+import { useCallback, useState } from "preact/hooks";
+import { InputCurrency } from "./InputCurrency";
+import { Input } from "./Input";
+import { TaxSchema as schema } from '../../schemas'
+import * as yup from 'yup';
+import { InputGroup } from "./InputGroup";
+
+export interface Props<T> {
+  name: keyof T;
+  readonly?: boolean;
+  isValid?: (e: any) => boolean;
+}
+type Entity = MerchantBackend.Tax
+export function InputTaxes<T>({ name, readonly }: Props<T>) {
+  const { error, value: taxes, onChange, } = useField<T>(name);
+
+  const [value, valueHandler] = useState<Partial<Entity>>({})
+  const [errors, setErrors] = useState<FormErrors<Entity>>({})
+
+  const submit = useCallback((): void => {
+    try {
+      schema.validateSync(value, { abortEarly: false })
+      onChange([value as any, ...taxes] as any)
+      valueHandler({})
+    } catch (err) {
+      const errors = err.inner as yup.ValidationError[]
+      const pathMessages = errors.reduce((prev, cur) => !cur.path ? prev : ({ 
...prev, [cur.path]: { type: cur.type, params: cur.params, message: cur.message 
} }), {})
+      setErrors(pathMessages)
+    }
+  }, [value])
+
+  return (
+    <InputGroup name="tax" alternative={taxes.length > 0 && <p>this product 
has {taxes.length} taxes</p>}>
+      <FormProvider<Entity> name="tax" errors={errors} object={value} 
valueHandler={valueHandler} >
+
+        <div class="field is-horizontal">
+          <div class="field-label is-normal">
+          </div>
+          <div class="field-body" style={{ display: 'block' }}>
+            {taxes.map((v: any) => <div class="tags has-addons mt-3 mb-0 mr-3" 
style={{ flexWrap: 'nowrap' }}>
+              <span class="tag is-medium is-info mb-0" style={{ maxWidth: 
'90%' }}><b>{v.tax}</b>: {v.name}</span>
+              <a class="tag is-medium is-danger is-delete mb-0" onClick={() => 
{
+                onChange(taxes.filter((f: any) => f !== v) as any);
+                valueHandler(v);
+              }} />
+            </div>
+            )}
+            {!taxes.length && 'this product has no taxes'}
+          </div>
+        </div>
+
+        <Input<Entity> name="tax" >
+          currency and value separated with colon <b>USD:2.3</b>
+        </Input>
+
+        <Input<Entity> name="name" />
+
+        <div class="buttons is-right mt-5">
+          <button class="button is-info" onClick={submit}>add</button>
+        </div>
+
+
+      </FormProvider>
+    </InputGroup>
+  )
+}
+
diff --git a/packages/frontend/src/components/form/InputWithAddon.tsx 
b/packages/frontend/src/components/form/InputWithAddon.tsx
index a983143..73f3fb1 100644
--- a/packages/frontend/src/components/form/InputWithAddon.tsx
+++ b/packages/frontend/src/components/form/InputWithAddon.tsx
@@ -33,21 +33,22 @@ export interface Props<T> {
   fromStr?: (s: string) => any;
   inputExtra?: any,
   children?: ComponentChildren,
+  side?: ComponentChildren;
 }
 
 const defaultToString = (f?: any):string => f || ''
 const defaultFromString = (v: string):any => v as any
 
-export function InputWithAddon<T>({ name, readonly, addonBefore, children, 
expand, inputType, inputExtra, addonAfter, toStr = defaultToString, fromStr = 
defaultFromString }: Props<T>): VNode {
-  const { error, value, onChange } = useField<T>(name);
+export function InputWithAddon<T>({ name, readonly, addonBefore, children, 
expand, inputType, inputExtra, side, addonAfter, toStr = defaultToString, 
fromStr = defaultFromString }: Props<T>): VNode {
+  const { error, value, onChange, formName } = useField<T>(name);
 
-  const placeholder = useMessage(`fields.instance.${name}.placeholder`);
-  const tooltip = useMessage(`fields.instance.${name}.tooltip`);
+  const placeholder = useMessage(`fields.${!formName ? 'instance' : 
formName}.${name}.placeholder`);
+  const tooltip = useMessage(`fields.${!formName ? 'instance' : 
formName}.${name}.tooltip`);
 
   return <div class="field is-horizontal">
     <div class="field-label is-normal">
       <label class="label">
-        <Message id={`fields.instance.${name}.label`} />
+        <Message id={`fields.${!formName ? 'instance' : 
formName}.${name}.label`} />
         {tooltip && <span class="icon" data-tooltip={tooltip}>
           <i class="mdi mdi-information" />
         </span>}
@@ -64,7 +65,7 @@ export function InputWithAddon<T>({ name, readonly, 
addonBefore, children, expan
               placeholder={placeholder} readonly={readonly} 
               name={String(name)} value={toStr(value)}
               onChange={(e): void => onChange(fromStr(e.currentTarget.value))} 
/>
-            <Message id={`fields.instance.${name}.help`}> </Message>
+            <Message id={`fields.${!formName ? 'instance' : 
formName}.${name}.help`}> </Message>
             {children}
           </p>
           {addonAfter && <div class="control">
@@ -73,6 +74,7 @@ export function InputWithAddon<T>({ name, readonly, 
addonBefore, children, expan
         </div>
         {error ? <p class="help is-danger"><Message 
id={`validation.${error.type}`} 
fields={error.params}>{error.message}</Message></p> : null}
       </div>
+        {side}
     </div>
   </div>;
 }
diff --git a/packages/frontend/src/components/product/ProductForm.tsx 
b/packages/frontend/src/components/product/ProductForm.tsx
index 94bb2b3..5071fac 100644
--- a/packages/frontend/src/components/product/ProductForm.tsx
+++ b/packages/frontend/src/components/product/ProductForm.tsx
@@ -5,37 +5,57 @@ import { Input } from "../form/Input";
 import { InputCurrency } from "../form/InputCurrency";
 import { useBackendContext, useConfigContext } from "../../context/backend";
 import { MerchantBackend } from "../../declaration";
-import { 
+import {
   ProductUpdateSchema as updateSchema,
   ProductCreateSchema as createSchema,
- } from '../../schemas'
+} from '../../schemas'
 import * as yup from 'yup';
-import { InputGroup } from "../form/InputGroup";
-import { useBackendURL } from "../../hooks";
 import { InputWithAddon } from "../form/InputWithAddon";
 import { InputImage } from "../form/InputImage";
+import { InputTaxes } from "../form/InputTaxes";
+import { InputStock, Stock } from "../form/InputStock";
 
-type Entity = MerchantBackend.Products.ProductAddDetail
+type Entity = MerchantBackend.Products.ProductDetail & { product_id: string }
 
 interface Props {
   onSubscribe: (c: () => Entity | undefined) => void;
   initial?: Partial<Entity>;
-  showId?: boolean;
+  alreadyExist?: boolean;
 }
 
-export function ProductForm({ onSubscribe, initial, showId }: Props) {
-  const [value, valueHandler] = useState<Partial<Entity>>(initial || {
+export function ProductForm({ onSubscribe, initial, alreadyExist, }: Props) {
+  const [value, valueHandler] = useState<Partial<Entity & { stock: Stock }>>({
     address: {},
     description_i18n: {},
     taxes: [],
-    next_restock: { t_ms: 'never' }
+    next_restock: { t_ms: 'never' },
+    ...initial,
+    stock: !initial || initial.total_stock === -1 ? undefined : {
+      current: initial.total_stock || 0,
+      lost: initial.total_lost || 0,
+      sold: initial.total_sold || 0,
+      address: initial.address,
+      nextRestock: initial.next_restock,
+    }
   })
   const [errors, setErrors] = useState<FormErrors<Entity>>({})
 
   const submit = useCallback((): Entity | undefined => {
     try {
-      (showId ? createSchema : updateSchema).validateSync(value, { abortEarly: 
false })
-      return value as MerchantBackend.Products.ProductAddDetail
+      (alreadyExist ?  updateSchema : createSchema).validateSync(value, { 
abortEarly: false })
+      const stock:Stock = (value as any).stock;
+      delete (value as any).stock;
+
+      if (!stock) {
+        value.total_stock = -1
+      } else {
+        value.total_stock = stock.current;
+        value.total_lost = stock.lost;
+        value.next_restock = stock.nextRestock instanceof Date ? { t_ms: 
stock.nextRestock.getTime() } : stock.nextRestock;
+        value.address = stock.address;
+      }
+      console.log(value)
+      return value as MerchantBackend.Products.ProductDetail & { product_id: 
string }
     } catch (err) {
       const errors = err.inner as yup.ValidationError[]
       const pathMessages = errors.reduce((prev, cur) => !cur.path ? prev : ({ 
...prev, [cur.path]: { type: cur.type, params: cur.params, message: cur.message 
} }), {})
@@ -43,39 +63,25 @@ export function ProductForm({ onSubscribe, initial, showId 
}: Props) {
     }
   }, [value])
 
-  const config = useConfigContext()
-
   useEffect(() => {
     onSubscribe(submit)
   }, [submit])
 
   const backend = useBackendContext();
+
   return <div>
-    <FormProvider<Entity> errors={errors} object={value} 
valueHandler={valueHandler} >
+    <FormProvider<Entity> name="product" errors={errors} object={value} 
valueHandler={valueHandler} >
 
-      { showId ? <InputWithAddon<Entity> name="product_id" 
addonBefore={`${backend.url}/product/`} /> : undefined }
+      {alreadyExist ? undefined : <InputWithAddon<Entity> name="product_id" 
addonBefore={`${backend.url}/product/`} /> }
+      
       <InputImage<Entity> name="image" />
       <Input<Entity> name="description" inputType="multiline" />
       <Input<Entity> name="unit" />
-      <InputCurrency<Entity> name="price" currency={config.currency} />
+      <InputCurrency<Entity> name="price" />
 
-      <Input<Entity> name="total_stock" inputType="number" fromStr={(v) => 
parseInt(v, 10)} toStr={(v) => "" + v} inputExtra={{ min: 0 }} />
+      <InputStock name="stock" alreadyExist={alreadyExist}/>
 
-      <InputGroup<Entity> name="address">
-        <Input name="address.country" />
-        <Input name="address.address_lines" inputType="multiline"
-          toStr={(v: string[] | undefined) => !v ? '' : v.join('\n')}
-          fromStr={(v: string) => v.split('\n')}
-        />
-        <Input name="address.building_number" />
-        <Input name="address.building_name" />
-        <Input name="address.street" />
-        <Input name="address.post_code" />
-        <Input name="address.town_location" />
-        <Input name="address.town" />
-        <Input name="address.district" />
-        <Input name="address.country_subdivision" />
-      </InputGroup>
+      <InputTaxes<Entity> name="taxes" />
 
     </FormProvider>
   </div>
diff --git a/packages/frontend/src/hooks/order.ts 
b/packages/frontend/src/hooks/order.ts
index da056dd..1d7330b 100644
--- a/packages/frontend/src/hooks/order.ts
+++ b/packages/frontend/src/hooks/order.ts
@@ -92,7 +92,13 @@ export function useOrderDetails(oderId: string): 
HttpResponse<MerchantBackend.Or
 
   const url = !admin ? baseUrl : `${baseUrl}/instances/${instanceId}`
 
-  const { data, error, isValidating } = 
useSWR<HttpResponseOk<MerchantBackend.Orders.MerchantOrderStatusResponse>, 
HttpError>([`/private/orders/${oderId}`, token, url], fetcher)
+  const { data, error, isValidating } = 
useSWR<HttpResponseOk<MerchantBackend.Orders.MerchantOrderStatusResponse>, 
HttpError>([`/private/orders/${oderId}`, token, url], fetcher, {
+    refreshInterval:0,
+    refreshWhenHidden: false,
+    revalidateOnFocus: false,
+    revalidateOnReconnect: false,
+    refreshWhenOffline: false,
+  })
 
   if (isValidating) return { loading: true, data: data?.data }
   if (data) return data
diff --git a/packages/frontend/src/hooks/product.ts 
b/packages/frontend/src/hooks/product.ts
index c74496d..8ae5b10 100644
--- a/packages/frontend/src/hooks/product.ts
+++ b/packages/frontend/src/hooks/product.ts
@@ -28,10 +28,7 @@ export function useProductAPI(): ProductAPI {
     await request(`${url}/private/products`, {
       method: 'post',
       token,
-      data: {
-        ...data,
-        image: {}
-      }
+      data
     });
 
     mutateAll(/@"\/private\/products"@/);
@@ -118,7 +115,13 @@ export function useProductDetails(productId: string): 
HttpResponse<MerchantBacke
   const url = !admin ? baseUrl : `${baseUrl}/instances/${instanceId}`
 
   const { data, error, isValidating } = 
useSWR<HttpResponseOk<MerchantBackend.Products.ProductDetail>, HttpError>(
-    [`/private/products/${productId}`, token, url], fetcher
+    [`/private/products/${productId}`, token, url], fetcher, {
+      refreshInterval:0,
+      refreshWhenHidden: false,
+      revalidateOnFocus: false,
+      revalidateOnReconnect: false,
+      refreshWhenOffline: false,  
+    }
   )
 
   if (isValidating) return { loading: true, data: data?.data }
diff --git a/packages/frontend/src/messages/en.po 
b/packages/frontend/src/messages/en.po
index cabbc68..da29624 100644
--- a/packages/frontend/src/messages/en.po
+++ b/packages/frontend/src/messages/en.po
@@ -260,18 +260,12 @@ msgstr "three state boolean"
 msgid "fields.instance.paid.label"
 msgstr "Paid"
 
-msgid "fields.instance.refunded.placeholder"
-msgstr ""
-
 #  msgid "fields.instance.refunded.tooltip"
 #  msgstr ""
 
 msgid "fields.instance.refunded.label"
 msgstr "Refunded"
 
-msgid "fields.instance.wired.placeholder"
-msgstr ""
-
 #  msgid "fields.instance.wired.tooltip"
 #  msgstr ""
 
@@ -311,9 +305,6 @@ msgstr "Description"
 msgid "fields.instance.description.placeholder"
 msgstr "add more information about the refund"
 
-msgid "fields.instance.order_status.placeholder"
-msgstr ""
-
 msgid "fields.instance.order_status.label"
 msgstr "Order status"
 
@@ -374,15 +365,6 @@ msgstr "Stock"
 msgid "fields.product.sold.label"
 msgstr "Sold"
 
-msgid "fields.instance.stock.label"
-msgstr "add stock"
-
-msgid "fields.instance.lost.label"
-msgstr "add stock lost"
-
-msgid "fields.instance.price.label"
-msgstr "new price"
-
 msgid "fields.instance.inventory_products.label"
 msgstr "Products from inventory"
 
@@ -520,4 +502,109 @@ msgstr "Unit"
 
 
 msgid "fields.instance.total_stock.label"
-msgstr "Total Stock"
\ No newline at end of file
+msgstr "Total Stock"
+
+
+msgid "fields.product.description.label"
+msgstr "Description"
+
+msgid "fields.product.unit.label"
+msgstr "Unit"
+
+msgid "fields.product.total_stock.label"
+msgstr "Total Stock"
+
+msgid "fields.product.product_id.label"
+msgstr "ID"
+
+msgid "fields.product.price.label"
+msgstr "Price"
+
+msgid "fields.tax.name.label"
+msgstr "Name"
+
+msgid "fields.tax.tax.label"
+msgstr "Amount"
+
+msgid "fields.groups.address.label"
+msgstr "Storage address"
+
+msgid "fields.stock.current.label"
+msgstr "Current stock"
+
+msgid "fields.stock.lost.label"
+msgstr "Lost stock"
+
+msgid "fields.stock.nextRestock.label"
+msgstr "Next Restock"
+
+
+msgid "fields.stock.address.country.label"
+msgstr "Country"
+
+msgid "fields.stock.address.country_subdivision.label"
+msgstr "Country Subdivision"
+
+msgid "fields.stock.address.district.label"
+msgstr "District"
+
+msgid "fields.stock.address.town.label"
+msgstr "Town"
+
+msgid "fields.stock.address.town_location.label"
+msgstr "Town Location"
+
+msgid "fields.stock.address.post_code.label"
+msgstr "Post code"
+
+msgid "fields.stock.address.street.label"
+msgstr "Street"
+
+msgid "fields.stock.address.building_name.label"
+msgstr "Building Name"
+
+msgid "fields.stock.address.building_number.label"
+msgstr "Building Number"
+
+msgid "fields.stock.address.address_lines.label"
+msgstr "Address"
+
+msgid "fields.groups.jurisdiction.label"
+msgstr "Jurisdiction"
+
+msgid "fields.groups.inventory_products.label"
+msgstr "Inventory Products"
+
+msgid "fields.groups.products.label"
+msgstr "Products"
+
+msgid "fields.groups.payments.label"
+msgstr "Payments"
+
+msgid "fields.groups.extra.label"
+msgstr "Extra"
+
+
+msgid "fields.instance.stock.label"
+msgstr "Stock"
+
+msgid "fields.instance.lost.label"
+msgstr "Lost"
+
+msgid "fields.instance.price.label"
+msgstr "Price"
+
+msgid "fields.groups.tax.label"
+msgstr "Taxes"
+
+msgid "validation.imageSizeLimit"
+msgstr "Image max size is 1 MB"
+
+msgid "fields.added.incoming.label"
+msgstr "Incoming"
+
+msgid "fields.added.lost.label"
+msgstr "Notify Lost"
+
+msgid "fields.added.price.label"
+msgstr "New Price"
diff --git a/packages/frontend/src/paths/admin/create/CreatePage.tsx 
b/packages/frontend/src/paths/admin/create/CreatePage.tsx
index 21cc219..aa8a778 100644
--- a/packages/frontend/src/paths/admin/create/CreatePage.tsx
+++ b/packages/frontend/src/paths/admin/create/CreatePage.tsx
@@ -77,7 +77,6 @@ export function CreatePage({ onCreate, isLoading, onBack, 
forceId }: Props): VNo
     }
   }
   const backend = useBackendContext()
-  const config = useConfigContext()
 
   return <div>
 
@@ -95,9 +94,9 @@ export function CreatePage({ onCreate, isLoading, onBack, 
forceId }: Props): VNo
 
             <InputPayto<Entity> name="payto_uris" />
 
-            <InputCurrency<Entity> name="default_max_deposit_fee" 
currency={config.currency} />
+            <InputCurrency<Entity> name="default_max_deposit_fee" />
 
-            <InputCurrency<Entity> name="default_max_wire_fee" 
currency={config.currency} />
+            <InputCurrency<Entity> name="default_max_wire_fee" />
 
             <Input<Entity> name="default_wire_fee_amortization" />
 
diff --git a/packages/frontend/src/paths/admin/list/Table.tsx 
b/packages/frontend/src/paths/admin/list/Table.tsx
index f0c3242..52c0fb5 100644
--- a/packages/frontend/src/paths/admin/list/Table.tsx
+++ b/packages/frontend/src/paths/admin/list/Table.tsx
@@ -96,7 +96,6 @@ function toggleSelected<T>(id: T): (prev: T[]) => T[] {
 }
 
 function Table({ rowSelection, rowSelectionHandler, instances, onUpdate, 
onDelete }: TableProps): VNode {
-  const { changeBackend, url } = useBackendContext()
   return (
     <div class="table-container">
     <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
diff --git a/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx 
b/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx
index 34079cf..1e6b9fd 100644
--- a/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx
+++ b/packages/frontend/src/paths/instance/orders/create/CreatePage.tsx
@@ -105,12 +105,12 @@ export function CreatePage({ onCreate, onBack }: Props): 
VNode {
           summary: order.pricing.summary,
           products: productList,
           extra: value.extra,
-          pay_deadline: value.payments.pay_deadline ? { t_ms: 
value.payments.pay_deadline.getTime() * 1000 } : undefined,
-          wire_transfer_deadline: value.payments.pay_deadline ? { t_ms: 
value.payments.pay_deadline.getTime() * 1000 } : undefined,
-          refund_deadline: value.payments.refund_deadline ? { t_ms: 
value.payments.refund_deadline.getTime() * 1000 } : undefined,
+          pay_deadline: value.payments.pay_deadline ? { t_ms: 
Math.floor(value.payments.pay_deadline.getTime()/1000)*1000 } : undefined,
+          wire_transfer_deadline: value.payments.pay_deadline ? { t_ms: 
Math.floor(value.payments.pay_deadline.getTime()/1000)*1000 } : undefined,
+          refund_deadline: value.payments.refund_deadline ? { t_ms: 
Math.floor(value.payments.refund_deadline.getTime()/1000)*1000 } : undefined,
           max_fee: value.payments.max_fee,
           max_wire_fee: value.payments.max_wire_fee,
-          delivery_date: value.payments.delivery_date ? { t_ms: 
value.payments.delivery_date.getTime() * 1000 } : undefined,
+          delivery_date: value.payments.delivery_date ? { t_ms: 
value.payments.delivery_date.getTime() } : undefined,
           delivery_location: value.payments.delivery_location,
         },
         inventory_products: inventoryList.map(p => ({
@@ -280,17 +280,17 @@ export function CreatePage({ onCreate, onBack }: Props): 
VNode {
           <FormProvider<Entity> errors={errors} object={value} 
valueHandler={valueHandler as any}>
             {hasProducts ?
               <Fragment>
-                <InputCurrency name="pricing.products_price" readonly 
currency={config.currency} />
-                <InputCurrency name="pricing.products_taxes" readonly 
currency={config.currency} />
-                <InputCurrency name="pricing.order_price" 
currency={config.currency}
+                <InputCurrency name="pricing.products_price" readonly />
+                <InputCurrency name="pricing.products_taxes" readonly />
+                <InputCurrency name="pricing.order_price"
                   addonAfter={value.pricing.order_price !== totalPrice && 
(discountOrRise < 1 ?
                     `discount of %${Math.round((1 - discountOrRise) * 100)}` :
                     `rise of %${Math.round((discountOrRise - 1) * 100)}`)
                   }
                 />
-                <InputCurrency name="pricing.net" readonly 
currency={config.currency} />
+                <InputCurrency name="pricing.net" readonly />
               </Fragment> :
-              <InputCurrency name="pricing.order_price" 
currency={config.currency} />
+              <InputCurrency name="pricing.order_price" />
             }
 
             <Input name="pricing.summary" inputType="multiline" />
@@ -317,8 +317,8 @@ export function CreatePage({ onCreate, onBack }: Props): 
VNode {
                 <Input name="payments.delivery_location.country_subdivision" />
               </InputGroup>}
 
-              <InputCurrency name="payments.max_fee" 
currency={config.currency} />
-              <InputCurrency name="payments.max_wire_fee" 
currency={config.currency} />
+              <InputCurrency name="payments.max_fee" />
+              <InputCurrency name="payments.max_wire_fee" />
               <Input name="payments.wire_fee_amortization" />
               <Input name="payments.fullfilment_url" />
             </InputGroup>
diff --git 
a/packages/frontend/src/paths/instance/orders/create/InventoryProductForm.tsx 
b/packages/frontend/src/paths/instance/orders/create/InventoryProductForm.tsx
index 73be77b..5ab94ee 100644
--- 
a/packages/frontend/src/paths/instance/orders/create/InventoryProductForm.tsx
+++ 
b/packages/frontend/src/paths/instance/orders/create/InventoryProductForm.tsx
@@ -1,10 +1,8 @@
 import { h } from "preact";
-import { Message } from "preact-messages";
 import { useState } from "preact/hooks";
 import { FormErrors, FormProvider } from "../../../../components/form/Field";
-import { Input } from "../../../../components/form/Input";
+import { InputNumber } from "../../../../components/form/InputNumber";
 import { InputSearchProduct } from 
"../../../../components/form/InputSearchProduct";
-import { InputWithAddon } from "../../../../components/form/InputWithAddon";
 import { MerchantBackend, WithId } from "../../../../declaration";
 import { ProductMap } from "./CreatePage";
 
@@ -52,9 +50,9 @@ export function InventoryProductForm({ currentProducts, 
onAddProduct }: Props) {
 
   return <FormProvider<Form> errors={errors} object={state} 
valueHandler={setState}>
     <InputSearchProduct selected={state.product} onChange={(p) => setState(v 
=> ({ ...v, product: p }))} />
-    <Input<Form> name="quantity" inputType="number" fromStr={(v) => 
parseInt(v, 10)} toStr={(v) => ""+v} inputExtra={{min:0}}/>
+    <InputNumber<Form> name="quantity" />
     <div class="buttons is-right mt-5">
-      <button class="button is-success" onClick={submit} >add</button>
+      <button class="button is-success" onClick={submit}>add</button>
     </div>
   </FormProvider>
 }
\ No newline at end of file
diff --git 
a/packages/frontend/src/paths/instance/orders/create/NonInventoryProductForm.tsx
 
b/packages/frontend/src/paths/instance/orders/create/NonInventoryProductForm.tsx
index 92b7413..21fba22 100644
--- 
a/packages/frontend/src/paths/instance/orders/create/NonInventoryProductForm.tsx
+++ 
b/packages/frontend/src/paths/instance/orders/create/NonInventoryProductForm.tsx
@@ -37,7 +37,8 @@ export function NonInventoryProductFrom({ value, onAddProduct 
}: Props) {
   const initial: Partial<MerchantBackend.Products.ProductAddDetail> = {
     ...value,
     total_stock: value?.quantity || 0,
-  } 
+    taxes: []
+  }
   
   return <Fragment>
     <div class="buttons">
diff --git a/packages/frontend/src/paths/instance/orders/details/DetailPage.tsx 
b/packages/frontend/src/paths/instance/orders/details/DetailPage.tsx
index c71aca5..23fe8dc 100644
--- a/packages/frontend/src/paths/instance/orders/details/DetailPage.tsx
+++ b/packages/frontend/src/paths/instance/orders/details/DetailPage.tsx
@@ -154,7 +154,7 @@ function ClaimedPage({ id, order }: { id: string; order: 
MerchantBackend.Orders.
                 <div class="title">Payment details</div>
                 <FormProvider<Claimed> errors={errors} object={value} 
valueHandler={valueHandler} >
                   <Input name="contract_terms.summary" readonly 
inputType="multiline" />
-                  <InputCurrency name="contract_terms.amount" readonly 
currency={config.currency} />
+                  <InputCurrency name="contract_terms.amount" readonly />
                   <Input<Claimed> name="order_status" readonly />
                 </FormProvider>
               </div>
@@ -233,7 +233,6 @@ function PaidPage({ id, order, onRefund }: { id: string; 
order: MerchantBackend.
   events.sort((a, b) => a.when.getTime() - b.when.getTime())
   const [value, valueHandler] = useState<Partial<Paid>>({ ...order, fee: 
'COL:0.1' } as any)
   const [errors, setErrors] = useState<KeyValue>({})
-  const config = useConfigContext()
 
   const refundable = new Date().getTime() < 
order.contract_terms.refund_deadline.t_ms
 
@@ -310,10 +309,10 @@ function PaidPage({ id, order, onRefund }: { id: string; 
order: MerchantBackend.
                 <div class="title">Payment details</div>
                 <FormProvider<Paid> errors={errors} object={value} 
valueHandler={valueHandler} >
                   <Input name="contract_terms.summary" readonly 
inputType="multiline" />
-                  <InputCurrency name="contract_terms.amount" readonly 
currency={config.currency} />
-                  <InputCurrency name="fee" readonly 
currency={config.currency} />
-                  {order.refunded && <InputCurrency<Paid> name="refund_amount" 
readonly currency={config.currency} />}
-                  <InputCurrency<Paid> name="deposit_total" readonly 
currency={config.currency} />
+                  <InputCurrency name="contract_terms.amount" readonly />
+                  <InputCurrency name="fee" readonly />
+                  {order.refunded && <InputCurrency<Paid> name="refund_amount" 
readonly />}
+                  <InputCurrency<Paid> name="deposit_total" readonly />
                   <Input<Paid> name="order_status" readonly />
                 </FormProvider>
               </div>
diff --git a/packages/frontend/src/paths/instance/orders/list/Table.tsx 
b/packages/frontend/src/paths/instance/orders/list/Table.tsx
index e0646cc..dc0fcae 100644
--- a/packages/frontend/src/paths/instance/orders/list/Table.tsx
+++ b/packages/frontend/src/paths/instance/orders/list/Table.tsx
@@ -167,7 +167,6 @@ interface RefundModalProps {
 }
 
 export function RefundModal({ id, onCancel, onConfirm }: RefundModalProps): 
VNode {
-  const config = useConfigContext()
   const result = useOrderDetails(id)
   type State = { mainReason?: string, description?: string, refund?: string }
   const [form, setValue] = useState<State>({})
@@ -226,7 +225,7 @@ export function RefundModal({ id, onCancel, onConfirm }: 
RefundModalProps): VNod
     </div>}
 
     { isRefundable && <FormProvider<State> errors={errors} object={form} 
valueHandler={(d) => setValue(d as any)}>
-      <InputCurrency<State> name="refund" currency={config.currency}>
+      <InputCurrency<State> name="refund">
         Max refundable: {totalRefundable}
       </InputCurrency>
       <InputSelector name="mainReason" values={['duplicated', 'requested by 
the customer', 'other']} />
diff --git 
a/packages/frontend/src/paths/instance/products/create/CreatePage.tsx 
b/packages/frontend/src/paths/instance/products/create/CreatePage.tsx
index 9df2c31..d5fff9d 100644
--- a/packages/frontend/src/paths/instance/products/create/CreatePage.tsx
+++ b/packages/frontend/src/paths/instance/products/create/CreatePage.tsx
@@ -25,7 +25,7 @@ import { Message } from "preact-messages";
 import { ProductForm } from "../../../../components/product/ProductForm";
 import { useListener } from "../../../../hooks";
 
-type Entity = MerchantBackend.Products.ProductAddDetail
+type Entity = MerchantBackend.Products.ProductDetail & { product_id: string}
 
 interface Props {
   onCreate: (d: Entity) => void;
@@ -44,7 +44,7 @@ export function CreatePage({ onCreate, onBack }: Props): 
VNode {
       <div class="columns">
         <div class="column" />
         <div class="column is-two-thirds">
-          <ProductForm onSubscribe={addFormSubmitter} showId />
+          <ProductForm onSubscribe={addFormSubmitter} />
 
           <div class="buttons is-right mt-5">
             {onBack && <button class="button" onClick={onBack} ><Message 
id="Cancel" /></button>}
diff --git 
a/packages/frontend/src/paths/instance/products/create/CreatedSuccessfully.tsx 
b/packages/frontend/src/paths/instance/products/create/CreatedSuccessfully.tsx
index 002c695..d6d82f9 100644
--- 
a/packages/frontend/src/paths/instance/products/create/CreatedSuccessfully.tsx
+++ 
b/packages/frontend/src/paths/instance/products/create/CreatedSuccessfully.tsx
@@ -13,7 +13,7 @@
  You should have received a copy of the GNU General Public License along with
  GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
  */
- import { h } from "preact";
+import { h } from "preact";
 import { CreatedSuccessfully as Template } from 
"../../../../components/notifications/CreatedSuccessfully";
 import { Entity } from "./index";
 
@@ -24,7 +24,7 @@ interface Props {
 }
 
 export function CreatedSuccessfully({ entity, onConfirm, onCreateAnother }: 
Props) {
-  
+
   return <Template onConfirm={onConfirm} onCreateAnother={onCreateAnother}>
     <div class="field is-horizontal">
       <div class="field-label is-normal">
@@ -33,7 +33,7 @@ export function CreatedSuccessfully({ entity, onConfirm, 
onCreateAnother }: Prop
       <div class="field-body is-flex-grow-3">
         <div class="field">
           <p class="control">
-            <img src={entity.image} />
+            <img src={entity.image} style={{ width: 200, height: 200 }} />
           </p>
         </div>
       </div>
diff --git a/packages/frontend/src/paths/instance/products/create/index.tsx 
b/packages/frontend/src/paths/instance/products/create/index.tsx
index 81faeeb..86652ab 100644
--- a/packages/frontend/src/paths/instance/products/create/index.tsx
+++ b/packages/frontend/src/paths/instance/products/create/index.tsx
@@ -47,7 +47,7 @@ export default function ({ onConfirm, onBack }: Props): VNode 
{
     <NotificationCard notification={notif} />
     <CreatePage
       onBack={onBack}
-      onCreate={(request: MerchantBackend.Products.ProductAddDetail) => {
+      onCreate={(request: MerchantBackend.Products.ProductDetail & { 
product_id: string}) => {
         createProduct(request).then(() => {
           setCreatedOk(request)
         }).catch((error) => {
diff --git a/packages/frontend/src/paths/instance/products/list/Table.tsx 
b/packages/frontend/src/paths/instance/products/list/Table.tsx
index f097648..bad5530 100644
--- a/packages/frontend/src/paths/instance/products/list/Table.tsx
+++ b/packages/frontend/src/paths/instance/products/list/Table.tsx
@@ -19,12 +19,14 @@
 * @author Sebastian Javier Marchano (sebasjm)
 */
 
-import { Fragment, h, VNode } from "preact"
+import { format } from "date-fns"
+import { ComponentChildren, Fragment, h, VNode } from "preact"
 import { Message } from "preact-messages"
 import { StateUpdater, useEffect, useState } from "preact/hooks"
 import { FormErrors, FormProvider } from "../../../../components/form/Field"
 import { Input } from "../../../../components/form/Input"
 import { InputCurrency } from "../../../../components/form/InputCurrency"
+import { InputNumber } from "../../../../components/form/InputNumber"
 import { useConfigContext } from "../../../../context/backend"
 import { MerchantBackend, WithId } from "../../../../declaration"
 import { useProductAPI } from "../../../../hooks/product"
@@ -80,13 +82,12 @@ interface TableProps {
 }
 
 function Table({ rowSelection, rowSelectionHandler, instances, onSelect, 
onUpdate, onDelete }: TableProps): VNode {
-  const { } = useProductAPI()
   return (
     <div class="table-container">
       <table class="table is-fullwidth is-striped is-hoverable is-fullwidth">
         <thead>
           <tr>
-            <th><Message id="fields.product.image.label" /></th>
+            <th><Message id="fields.product.image.label" style={{ with: 100 }} 
/></th>
             <th><Message id="fields.product.description.label" /></th>
             <th><Message id="fields.product.sell.label" /></th>
             <th><Message id="fields.product.taxes.label" /></th>
@@ -98,13 +99,27 @@ function Table({ rowSelection, rowSelectionHandler, 
instances, onSelect, onUpdat
         </thead>
         <tbody>
           {instances.map(i => {
+
+            let restStockInfo = !i.next_restock ? '' : (
+              i.next_restock.t_ms === 'never' ?
+                'never' :
+                `restock at ${format(new Date(i.next_restock.t_ms), 
'yyyy/MM/dd')}`
+            )
+            let stockInfo: ComponentChildren = '';
+            if (i.total_stock < 0) {
+              stockInfo = 'infinite'
+            } else {
+              const totalStock = i.total_stock - i.total_lost - i.total_sold
+              stockInfo = <label title={restStockInfo}>{totalStock} 
{i.unit}</label>
+            }
+
             return <Fragment><tr>
-              <td onClick={() => rowSelection !== i.id && 
rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} 
>{JSON.stringify(i.image)}</td>
+              <td onClick={() => rowSelection !== i.id && 
rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} ><img src={i.image} 
style={{ border: 'solid black 1px', width: 100, height: 100 }} /></td>
               <td onClick={() => rowSelection !== i.id && 
rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} >{i.description}</td>
               <td onClick={() => rowSelection !== i.id && 
rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} >{i.price} / 
{i.unit}</td>
               <td onClick={() => rowSelection !== i.id && 
rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} >{sum(i.taxes)}</td>
               <td onClick={() => rowSelection !== i.id && 
rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} >{difference(i.price, 
sum(i.taxes))}</td>
-              <td onClick={() => rowSelection !== i.id && 
rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} >{i.total_stock} 
{i.unit} ({i.next_restock?.t_ms})</td>
+              <td onClick={() => rowSelection !== i.id && 
rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} >{stockInfo}</td>
               <td onClick={() => rowSelection !== i.id && 
rowSelectionHandler(i.id)} style={{ cursor: 'pointer' }} >{i.total_sold} 
{i.unit}</td>
               <td class="is-actions-cell right-sticky">
                 <div class="buttons is-right">
@@ -136,51 +151,62 @@ interface FastProductUpdateFormProps {
   onCancel: () => void;
 }
 interface FastProductUpdate {
-  stock?: number;
-  lost?: number;
-  price?: string;
+  incoming: number;
+  lost: number;
+  price: string;
 }
 
 function FastProductUpdateForm({ product, onUpdate, onCancel }: 
FastProductUpdateFormProps) {
-  const [value, valueHandler] = useState<FastProductUpdate>({})
-  const config = useConfigContext()
+  const [value, valueHandler] = useState<FastProductUpdate>({
+    incoming: 0, lost: 0, price: product.price
+  })
+
+  const currentStock = product.total_stock - product.total_sold - 
product.total_lost
 
-  const errors:FormErrors<FastProductUpdate> = {
-    lost: !value.lost ? undefined : (value.lost < product.total_lost ? 
{message: `should be greater than ${product.total_lost}`} : undefined),
-    stock: !value.stock ? undefined : (value.stock < product.total_stock ? 
{message: `should be greater than ${product.total_stock}`} : undefined),
-    price: undefined,
+  const errors: FormErrors<FastProductUpdate> = {
+    lost: currentStock + value.incoming < value.lost ? {
+      message: `lost cannot be greater that current + incoming (max 
${currentStock + value.incoming})`
+    } : undefined
   }
 
+  const stockUpdateDescription = errors.lost ? '' : (
+    !!value.incoming || !!value.lost ?
+      `current stock will change from ${currentStock} to ${currentStock + 
value.incoming - value.lost}` :
+      `current stock will stay at ${currentStock}`
+  )
+
   const hasErrors = Object.keys(errors).some(k => (errors as any)[k] !== 
undefined)
   const isDirty = Object.keys(value).some(k => !!(value as any)[k])
 
   return <Fragment>
-    <FormProvider<FastProductUpdate> errors={errors} object={value} 
valueHandler={valueHandler} >
-      <div class="columns">
-        <div class="column">
-          <Input<FastProductUpdate> name="stock" inputType="number" 
fromStr={(v) => parseInt(v, 10)} toStr={(v) => ""+v}/>
-        </div>
-        <div class="column">
-          <Input<FastProductUpdate> name="lost" inputType="number" 
fromStr={(v) => parseInt(v, 10)} toStr={(v) => ""+v}/>
-        </div>
-        <div class="column">
-          <InputCurrency<FastProductUpdate> name="price" 
currency={config.currency} />
+    <FormProvider<FastProductUpdate> name="added" errors={errors} 
object={value} valueHandler={valueHandler as any} >
+      <InputNumber<FastProductUpdate> name="incoming" />
+      <InputNumber<FastProductUpdate> name="lost" />
+      <div class="field is-horizontal">
+        <div class="field-label is-normal"></div>
+        <div class="field-body is-flex-grow-3">
+          <div class="field">
+            {stockUpdateDescription}
+          </div>
         </div>
       </div>
+      <InputCurrency<FastProductUpdate> name="price" />
     </FormProvider>
 
     <div class="buttons is-right mt-5">
       <button class="button" onClick={onCancel} ><Message id="Cancel" 
/></button>
       <button class="button is-info" disabled={hasErrors || !isDirty} 
onClick={() => {
+
         return onUpdate({
           ...product,
-          total_stock: value.stock || product.total_stock,
-          total_lost: value.lost || product.total_lost,
-          price: value.price || product.price,
+          total_stock: product.total_stock + value.incoming,
+          total_lost: product.total_lost+ value.lost,
+          price: value.price,
         })
+
       }}><Message id="Confirm" /></button>
     </div>
-    
+
   </Fragment>
 
 }
diff --git a/packages/frontend/src/paths/instance/products/list/index.tsx 
b/packages/frontend/src/paths/instance/products/list/index.tsx
index c5f5564..baef0c2 100644
--- a/packages/frontend/src/paths/instance/products/list/index.tsx
+++ b/packages/frontend/src/paths/instance/products/list/index.tsx
@@ -49,13 +49,6 @@ export default function ({ onUnauthorized, onLoadError, 
onCreate, onSelect, onNo
   if (!result.ok) return onLoadError(result)
 
   return <section class="section is-main-section">
-    <NotificationCard notification={{
-      message: 'DEMO',
-      type: 'WARN',
-      description: <ul>
-        <li>image return object when api says string</li>
-      </ul>
-    }} />
     <NotificationCard notification={notif} />
 
     <CardTable instances={result.data}
diff --git 
a/packages/frontend/src/paths/instance/products/update/UpdatePage.tsx 
b/packages/frontend/src/paths/instance/products/update/UpdatePage.tsx
index 40319a8..70f11b3 100644
--- a/packages/frontend/src/paths/instance/products/update/UpdatePage.tsx
+++ b/packages/frontend/src/paths/instance/products/update/UpdatePage.tsx
@@ -25,7 +25,7 @@ import { Message } from "preact-messages";
 import { ProductForm } from "../../../../components/product/ProductForm";
 import { useListener } from "../../../../hooks";
 
-type Entity = MerchantBackend.Products.ProductPatchDetail & WithId
+type Entity = MerchantBackend.Products.ProductDetail & WithId
 
 interface Props {
   onUpdate: (d: Entity) => void;
@@ -48,7 +48,7 @@ export function UpdatePage({ product, onUpdate, onBack }: 
Props): VNode {
               const p = a()
               return p as any
             })
-          }} />
+          }} alreadyExist />
 
           <div class="buttons is-right mt-5">
             {onBack && <button class="button" onClick={onBack} ><Message 
id="Cancel" /></button>}
diff --git a/packages/frontend/src/paths/instance/update/UpdatePage.tsx 
b/packages/frontend/src/paths/instance/update/UpdatePage.tsx
index e77bd38..8b5f7db 100644
--- a/packages/frontend/src/paths/instance/update/UpdatePage.tsx
+++ b/packages/frontend/src/paths/instance/update/UpdatePage.tsx
@@ -93,7 +93,6 @@ export function UpdatePage({ onUpdate, isLoading, selected, 
onBack }: Props): VN
       setErrors(pathMessages)
     }
   }
-  const config = useConfigContext()
 
   return <div>
     <section class="section is-main-section">
@@ -108,9 +107,9 @@ export function UpdatePage({ onUpdate, isLoading, selected, 
onBack }: Props): VN
 
             <InputPayto<Entity> name="payto_uris" />
 
-            <InputCurrency<Entity> name="default_max_deposit_fee" 
currency={config.currency} />
+            <InputCurrency<Entity> name="default_max_deposit_fee" />
 
-            <InputCurrency<Entity> name="default_max_wire_fee" 
currency={config.currency} />
+            <InputCurrency<Entity> name="default_max_wire_fee" />
 
             <Input<Entity> name="default_wire_fee_amortization" 
inputType="number" />
 
diff --git a/packages/frontend/src/schemas/index.ts 
b/packages/frontend/src/schemas/index.ts
index 01209df..54a3a8e 100644
--- a/packages/frontend/src/schemas/index.ts
+++ b/packages/frontend/src/schemas/index.ts
@@ -37,20 +37,14 @@ function listOfPayToUrisAreValid(values?: (string | 
undefined)[]): boolean {
   return !!values && values.every(v => v && PAYTO_REGEX.test(v));
 }
 
-// function numberToDuration(this: yup.AnySchema, current: any, original: 
string): Duration {
-//   if (this.isType(current)) return current;
-//   const d_ms = parseInt(original, 10) * 1000
-//   return { d_ms }
-// }
-
 function currencyWithAmountIsValid(value?: string): boolean {
   return !!value && AMOUNT_REGEX.test(value)
 }
 function currencyGreaterThan0(value?: string) {
   if (value) {
     try {
-      const [,amount] = value.split(':')
-      const intAmount = parseInt(amount,10)
+      const [, amount] = value.split(':')
+      const intAmount = parseInt(amount, 10)
       return intAmount > 0
     } catch {
       return false
@@ -160,7 +154,7 @@ export const OrderCreateSchema = yup.object().shape({
       .test('future', 'should be in the future', (d) => d ? isFuture(d) : 
true),
   }).test('payment', 'dates', (d) => {
     if (d.pay_deadline && d.refund_deadline && isAfter(d.refund_deadline, 
d.pay_deadline)) {
-      return new yup.ValidationError('pay deadline should be greater than 
refund','asd','payments.pay_deadline')
+      return new yup.ValidationError('pay deadline should be greater than 
refund', 'asd', 'payments.pay_deadline')
     }
     return true
   })
@@ -173,7 +167,9 @@ export const ProductCreateSchema = yup.object().shape({
   price: yup.string()
     .required()
     .test('amount', 'the amount is not valid', currencyWithAmountIsValid),
-  total_stock: yup.number().required(),
+  stock: yup.object({
+
+  }).optional(),
 })
 
 export const ProductUpdateSchema = yup.object().shape({
@@ -181,5 +177,15 @@ export const ProductUpdateSchema = yup.object().shape({
   price: yup.string()
     .required()
     .test('amount', 'the amount is not valid', currencyWithAmountIsValid),
-  total_stock: yup.number().required(),
+  stock: yup.object({
+
+  }).optional(),
 })
+
+
+export const TaxSchema = yup.object().shape({
+  name: yup.string().required().ensure(),
+  tax: yup.string()
+    .required()
+    .test('amount', 'the amount is not valid', currencyWithAmountIsValid),
+})
\ No newline at end of file

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]