Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
0ab7922
feat: Reconciliation page
lauty95 Mar 11, 2025
438e872
fix: unused type
lauty95 Mar 11, 2025
61ccc2e
fix: styles
lauty95 Mar 11, 2025
c2f0fc6
fix: mapper var
lauty95 Mar 11, 2025
9273295
feat: handling 'Stock Reconciliation Item' WH scan
lauty95 Mar 11, 2025
1dacb43
prettier
lauty95 Mar 12, 2025
1192630
Merge remote-tracking branch 'upstream/mobile_v15' into mobile_v15-st…
Alchez May 27, 2025
b713ddb
feat: fetching items
lauty95 May 30, 2025
b63812c
feat: custom get_items
lauty95 May 30, 2025
5f990f9
linters
lauty95 May 30, 2025
874b485
print
lauty95 May 30, 2025
0f7976c
logs: warehouses fetch
lauty95 Jun 24, 2025
fd1a891
feat: waiting stonecrop new version
lauty95 Jun 24, 2025
b034216
logs: warehouses fetch
lauty95 Jun 24, 2025
bd95705
feat: buttons and listview update
lauty95 Jun 24, 2025
426035a
feat: buttons
lauty95 Jun 24, 2025
9770a36
linters
lauty95 Jun 24, 2025
2639ca2
fix: get posible None
lauty95 Jun 24, 2025
6302505
fix: white button
lauty95 Jun 24, 2025
d539d6e
feat: clear items with X button
lauty95 Jun 27, 2025
3a173b6
own tests
lauty95 Jul 2, 2025
dd2079d
fix: handling items
lauty95 Jul 4, 2025
ac4130a
feat: stock_uom on scan
lauty95 Jul 4, 2025
a4cf479
feat: handling scanning
lauty95 Jul 4, 2025
6777bae
linters
lauty95 Jul 4, 2025
4f7e0c5
feat: stonecrop dependency upgrade
lauty95 Jul 17, 2025
356cbfd
feat: items filtered by valuation_rate
lauty95 Jul 17, 2025
aa88cdb
linters
lauty95 Jul 17, 2025
c5e3e1f
Merge mobile_v15 into mobile_v15-stock-reconciliation-mobile
lauty95 Dec 5, 2025
d9789c6
fix: node-version
lauty95 Dec 5, 2025
57ef4ca
fix: node-version pytest
lauty95 Dec 5, 2025
dc6b4f3
test: useHttpStore headers and body changes
lauty95 Dec 5, 2025
596c2a7
fix: no computed header
lauty95 Dec 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/playwright.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 22
check-latest: true
cache: 'yarn'

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pytest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 22
check-latest: true
cache: 'yarn'

Expand Down
43 changes: 43 additions & 0 deletions beam/beam/overrides/stock_reconciliation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Copyright (c) 2024, AgriTheory and contributors
# For license information, please see license.txt

import frappe
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import StockReconciliation


class BEAMStockReconciliation(StockReconciliation):
pass


@frappe.whitelist()
def get_items(
warehouse, posting_date, posting_time, company, item_code=None, ignore_empty_stock=False
):
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
get_items as erpnext_get_items,
)
from erpnext.stock.doctype.item.item import get_item_defaults
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions

items = erpnext_get_items(
warehouse, posting_date, posting_time, company, item_code, ignore_empty_stock
)
dimensions = get_inventory_dimensions()

filtered_items = []

for item in items:
if item.get("valuation_rate", 0) == 0:
continue

item_defaults = get_item_defaults(item["item_code"], company)
item["stock_uom"] = item_defaults.get("stock_uom")

for dim in dimensions:
fieldname = dim.get("fieldname")
if fieldname and fieldname in item_defaults:
item[fieldname] = item_defaults[fieldname]

filtered_items.append(item)

return filtered_items
7 changes: 7 additions & 0 deletions beam/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,7 @@
"PurchaseReceipt": "./beam/beam/www/beam/pages/PurchaseReceipt.vue",
"Receive": "./beam/beam/www/beam/pages/Receive.vue",
"Repack": "./beam/beam/www/beam/pages/Repack.vue",
"Reconciliation": "./beam/beam/www/beam/pages/Reconciliation.vue",
"Ship": "./beam/beam/www/beam/pages/Ship.vue",
"WorkOrder": "./beam/beam/www/beam/pages/WorkOrder.vue",
"Workstation": "./beam/beam/www/beam/pages/Workstation.vue",
Expand Down Expand Up @@ -642,6 +643,12 @@
"component": "Repack",
"meta": {"requiresAuth": True, "doctype": "Stock Entry", "view": "form"},
},
{
"path": "/stock-reconciliation",
"name": "stock-reconciliation",
"component": "Reconciliation",
"meta": {"requiresAuth": True, "doctype": "Stock Reconciliation", "view": "form"},
},
{
"path": "/:catchAll(.*)*",
"name": "404",
Expand Down
6 changes: 6 additions & 0 deletions beam/tests/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,12 @@ def setup_beam_settings(settings):
{"label": "Receive", "route": "#/receive", "dt": "Purchase Receipt", "component": "Receive"},
{"label": "Ship", "route": "#/ship", "dt": "Delivery Note", "component": "Ship"},
{"label": "Repack", "route": "#/repack", "dt": "Stock Entry", "component": "Repack"},
{
"label": "Reconciliation",
"route": "#/stock-reconciliation",
"dt": "Stock Reconciliation",
"component": "Reconciliation",
},
],
)
beams.save()
Expand Down
Empty file.
Empty file.
3 changes: 0 additions & 3 deletions beam/www/beam/pages/Move.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,6 @@ import ControlButtons from '@/components/ControlButtons.vue'
import { useBeamStore } from '@/stores/beam'
import type { ControlButton, DocActionResponse, StockEntry } from '@/types'
import { watch } from 'vue'
type Warehouse = {
name: string
}

const store = useBeamStore()
const items = ref<ListViewItem[]>([])
Expand Down
225 changes: 225 additions & 0 deletions beam/www/beam/pages/Reconciliation.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
<template>
<Navbar>
<template #title>
<h1>Reconciliation</h1>
</template>
<template #navbaraction>
<RouterLink :to="{ name: 'home' }">Home</RouterLink>
</template>
</Navbar>

<div class="reconciliation">
<div class="dropdown-container">
<ADropdown label="Warehouse" :items="warehouseList" v-model="reconciliation.set_warehouse" />
<BeamBtn class="clear-button" @click="clearField()"> X </BeamBtn>
</div>
</div>

<!-- body section -->
<ListView v-if="items.length > 0" :items="items" :key="componentKey" @update="updateItem" />
<div class="begin" v-else>
<span>Scan or Select Warehouses to Begin</span>
</div>
<!-- footer section -->
<ControlButtons :buttons="controlButtons" />
</template>

<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
import ControlButtons from '@/components/ControlButtons.vue'
import { useBeamStore } from '@/stores/beam'
import type { ControlButton, StockReconciliation, StockEntryItem } from '@/types'
type StockEntryItemWithCount = StockEntryItem & { count?: { count: number; of: number; uom?: string } }

const store = useBeamStore()
const componentKey = ref(0)
const reconciliation = computed(
(): StockReconciliation =>
(store.cache.mappers['stock-reconciliation'] as StockReconciliation) || {
name: '',
purpose: 'Stock Reconciliation',
items: [],
set_warehouse: '',
}
)
const items = ref([] as StockEntryItem[])
const warehouseList = ref<string[]>([])

const clearField = () => {
store.$patch(state => (state.cache.mappers['stock-reconciliation']['set_warehouse'] = ''))
store.$patch(state => ((state.cache.mappers['stock-reconciliation'] as StockReconciliation).items = []))
items.value = []
}

const mergeItems = (newItems: StockEntryItem[], fromWarehouse?: Boolean) => {
if (!newItems) return

newItems.forEach(newItem => {
const existingIndex = items.value.findIndex(item => item.item_code === newItem.item_code)

if (existingIndex !== -1) {
const existingItem = items.value[existingIndex] as StockEntryItemWithCount
const currentCount = existingItem.count?.count || 0
const incrementBy = fromWarehouse ? newItem.qty || 1 : 1
const count = currentCount > 0 ? currentCount + incrementBy : newItem.qty || 1

items.value[existingIndex] = {
...existingItem,
...newItem,
label: newItem.item_code,
count: {
count,
of: 0,
uom: existingItem.count?.uom || newItem.stock_uom,
},
debounce: 1000,
linkComponent: 'ListCount',
} as StockEntryItemWithCount
} else {
items.value.push({
...newItem,
label: newItem.item_code,
count: {
count: newItem.qty || 1,
of: 0,
uom: newItem.stock_uom,
},
debounce: 1000,
linkComponent: 'ListCount',
} as StockEntryItemWithCount)
}
})

componentKey.value++
}

const loadItemsFromWarehouse = async warehouse => {
if (!warehouse) return
try {
let response = await store.getStockReconciliationItems(warehouse)
mergeItems(response || [], true)
} catch (error) {
console.error('Error loading items:', error)
}
}

const updateItem = (value: StockEntryItemWithCount) => {
if (!value || !value.count || !value.count.count) return
for (const item of reconciliation.value.items) {
if (item.item_code === value.item_code && item.qty !== value.count.count) {
item.qty = value.count.count
break
}
}
}

const create = async () => {
const body = {
purpose: 'Stock Reconciliation',
set_warehouse: reconciliation.value.set_warehouse,
items: (items.value as StockEntryItemWithCount[]).map(i => ({
...i,
qty: typeof i.count === 'object' ? i.count.count : i.qty,
})),
name: reconciliation.value.name,
}

let res
if (body.name) {
res = await store.update('Stock Reconciliation', body.name, body)
} else {
res = await store.insert('Stock Reconciliation', body)
}
const { data } = res
if (data && data.name) reconciliation.value.name = data.name
return res
}

const submit = async () => {
if (!reconciliation.value.name) return
const res = await store.submit('Stock Reconciliation', reconciliation.value.name)
if (res?.data) {
store.$patch(state => {
;(state.cache.mappers['stock-reconciliation'] as any) = {
name: '',
purpose: 'Stock Reconciliation',
items: [],
set_warehouse: '',
}
})
items.value = []
componentKey.value++
}
}

const cancel = async () => {
store.$patch(state => {
;(state.cache.mappers['stock-reconciliation'] as any) = {
name: '',
purpose: 'Stock Reconciliation',
items: [],
set_warehouse: '',
}
})
items.value = []
componentKey.value++
}

const controlButtons = computed((): ControlButton[] => {
if (!items.value.length || !reconciliation.value.set_warehouse) return []
return [
{
label: 'SAVE',
disabled: items.value.length === 0,
color: { background: '#4791FF', text: 'var(--sc-btn-color)' },
action: create,
},
{
label: 'SUBMIT',
disabled: !reconciliation.value.name,
hidden: !reconciliation.value.name,
color: { background: 'var(--sc-success)', text: 'var(--sc-btn-color)' },
action: submit,
},
{
label: 'CANCEL',
color: { background: 'white', text: 'black' },
action: cancel,
},
]
})

watch(
() => reconciliation.value.set_warehouse,
warehouse => loadItemsFromWarehouse(warehouse)
)

watch(
() => store.cache.mappers['stock-reconciliation']?.items,
newItems => mergeItems(newItems || []),
{ immediate: true, deep: true }
)

onMounted(async () => {
store.$patch(state => ((state.cache.mappers['stock-reconciliation'] as StockReconciliation) = reconciliation.value))
warehouseList.value = store.warehouseList.filter(w => !w.is_group).map(w => w.name)
})
</script>
<style>
.reconciliation {
margin-bottom: 1.5em;
}

.reconciliation .autocomplete input,
.autocomplete-results {
font-size: 150%;
}

.autocomplete-results {
padding-inline: 3px !important;
}

.reconciliation .input-wrapper label {
margin: calc(-2.5rem - calc(2.15rem / 2)) 0 0 1ch !important;
}
</style>
37 changes: 34 additions & 3 deletions beam/www/beam/stores/beam.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,18 @@ export const useBeamStore = defineStore('beam', () => {
}

const setWarehouses = async () => {
warehouseList.value = await getAll<{ name: string }[]>('Warehouse', {
fields: JSON.stringify(['company', 'disabled', 'is_group', 'name', 'warehouse_name']),
})
if (warehouseList.value && warehouseList.value.length) return

try {
const warehouses = await getAll<{ name: string }[]>('Warehouse', {
fields: JSON.stringify(['company', 'disabled', 'is_group', 'name', 'warehouse_name']),
})
console.log('Warehouses fetched:', warehouses)
warehouseList.value = warehouses
} catch (error) {
console.error('Error fetching warehouses:', error)
warehouseList.value = []
}
}

const getOne = async <T>(doctype: string, name: string) => {
Expand Down Expand Up @@ -280,6 +289,27 @@ export const useBeamStore = defineStore('beam', () => {
}
}

const getStockReconciliationItems = async (warehouse: string) => {
if (!warehouse) return []
try {
const homeData = await getHome()
const company = homeData.data.company
const response = await httpStore.get('/api/method/beam.beam.overrides.stock_reconciliation.get_items', {
warehouse,
company,
posting_date: new Date().toLocaleDateString(),
posting_time: new Date().toLocaleTimeString(),
ignore_empty_stock: true,
})

const { message } = await response.json()
return message
} catch (error) {
console.error(error)
return []
}
}

const logout = async () => {
await httpStore.get(LOGOUT_URL)
window.location.href = '/login?redirect-to=/beam#'
Expand Down Expand Up @@ -327,6 +357,7 @@ export const useBeamStore = defineStore('beam', () => {
getOne,
getReceiving,
getStockEntryItems,
getStockReconciliationItems,
logout,
makeNewDoc,
scan,
Expand Down
Loading
Loading