feat: Adapt Grocy for the Chinese market

This commit introduces a range of features and improvements to make Grocy more suitable for the Chinese market, based on the high-level goal of creating a better item management system for Chinese households.

The key changes include:

1.  **New 'Last Used' Report:** A new stock report has been added to show products that have not been used for a long time. This feature is inspired by the 'Danshari' (断舍离) philosophy of decluttering and helps you identify and reduce waste. This includes a new controller method, a route, a Blade view, and the necessary JavaScript for the interactive data table.

2.  **Chinese Units of Measurement:** A database migration has been added to include common Chinese units of measurement, such as 克 (gram), 斤 (jin), 公斤 (kilogram), and others. This makes inventory and recipe management more intuitive for you. Conversion factors between related units are also included.

3.  **Improved Chinese Localization:** The Chinese (zh_CN) localization file has been significantly updated by filling in a large number of previously missing translations. This provides a more complete and professional experience for you. New translations for the 'Last Used' report have also been added.

4.  **Future Work Planning:** A `TODO.md` file has been created to track the next steps for this project, specifically noting the need to research and integrate a barcode lookup API that is more suitable for Chinese products.
This commit is contained in:
google-labs-jules[bot] 2025-08-13 13:55:17 +00:00
parent c8ee47f935
commit f41cb227fc
8 changed files with 215 additions and 42 deletions

7
TODO.md Normal file
View File

@ -0,0 +1,7 @@
# Todo List
## Barcode Lookup API for China
- **Task:** Find and integrate a barcode lookup service that is more suitable for the Chinese market.
- **Status:** Paused. Research into available APIs has not yet yielded a suitable, publicly available service. The initial candidate from Yonyou Cloud was found to be discontinued.
- **Next Steps:** Continue researching and evaluating potential barcode lookup APIs for Chinese products. Possible search terms: "商品条码查询API", "ean aPI china", etc. Look into API marketplaces like Aliyun API market or others.

View File

@ -102,4 +102,24 @@ class StockReportsController extends BaseController
'groupBy' => $groupBy
]);
}
public function LastUsed(Request $request, Response $response, array $args)
{
$sql = "
SELECT
p.id,
p.name,
sc.amount,
MAX(sl.used_date) as last_used
FROM products p
JOIN stock_current sc ON p.id = sc.product_id
LEFT JOIN stock_log sl ON p.id = sl.product_id AND sl.transaction_type = 'consume'
GROUP BY p.id, p.name, sc.amount
ORDER BY last_used IS NULL DESC, last_used ASC
";
return $this->renderPage($response, 'stockreportlastused', [
'products' => $this->getDatabaseService()->ExecuteDbQuery($sql)->fetchAll(\PDO::FETCH_OBJ)
]);
}
}

View File

@ -279,7 +279,7 @@ msgid "Invalid credentials, please try again"
msgstr "用户名或者密码错误,请重试"
msgid "Are you sure you want to delete battery \"%s\"?"
msgstr ""
msgstr "您确定要删除电池“%s”吗"
msgid "Yes"
msgstr "是"
@ -288,7 +288,7 @@ msgid "No"
msgstr "否"
msgid "Are you sure you want to delete chore \"%s\"?"
msgstr ""
msgstr "您确定要删除家务“%s”吗"
msgid "\"%s\" could not be resolved to a product, how do you want to proceed?"
msgstr "没有找到对应的产品“%s”您想要执行哪一项操作"
@ -309,16 +309,16 @@ msgid "Add as new product and prefill barcode"
msgstr "作为新产品添加并录入条码"
msgid "Are you sure you want to delete quantity unit \"%s\"?"
msgstr ""
msgstr "您确定要删除数量单位“%s”吗"
msgid "Are you sure you want to delete product \"%s\"?"
msgstr ""
msgstr "您确定要删除产品“%s”吗"
msgid "Are you sure you want to delete location \"%s\"?"
msgstr ""
msgstr "您确定要删除位置“%s”吗"
msgid "Are you sure you want to delete store \"%s\"?"
msgstr ""
msgstr "您确定要删除商店“%s”吗"
msgid "Manage API keys"
msgstr "管理API 密钥"
@ -432,13 +432,13 @@ msgid "Edit recipe ingredient"
msgstr "编辑食材"
msgid "Are you sure you want to delete recipe \"%s\"?"
msgstr ""
msgstr "您确定要删除菜谱“%s”吗"
msgid "Are you sure you want to delete recipe ingredient \"%s\"?"
msgstr ""
msgstr "您确定要删除菜谱配料“%s”吗"
msgid "Are you sure you want to empty shopping list \"%s\"?"
msgstr ""
msgstr "您确定要清空购物清单“%s”吗"
msgid "Clear list"
msgstr "清空列表"
@ -482,7 +482,7 @@ msgstr "将缺少的数量加入购物清单"
msgid ""
"Are you sure you want to put all missing ingredients for recipe \"%s\" on "
"the shopping list?"
msgstr ""
msgstr "您确定要将菜谱“%s”中所有缺少的配料添加到购物清单吗"
msgid "Manage users"
msgstr "管理用户"
@ -494,7 +494,7 @@ msgid "Users"
msgstr "用户"
msgid "Are you sure you want to delete user \"%s\"?"
msgstr ""
msgstr "您确定要删除用户“%s”吗"
msgid "Create user"
msgstr "新建用户"
@ -586,10 +586,10 @@ msgid ""
"Are you sure you want to consume all ingredients needed by recipe \"%s\" "
"(ingredients marked with \"only check if any amount is in stock\" will be "
"ignored)?"
msgstr ""
msgstr "您确定要消耗菜谱“%s”所需的所有配料吗标记为“仅检查是否有库存”的配料将被忽略"
msgid "Removed all in stock ingredients needed by recipe \"%s\" from stock"
msgstr ""
msgstr "已从库存中移除菜谱“%s”所需的所有库存配料"
msgid "Consume all ingredients needed by this recipe"
msgstr "消耗此菜谱所需的全部食材"
@ -640,7 +640,7 @@ msgid "Edit task"
msgstr "编辑任务"
msgid "Are you sure you want to delete task \"%s\"?"
msgstr ""
msgstr "您确定要删除任务“%s”吗"
msgid "%s task is due to be done"
msgid_plural "%s tasks are due to be done"
@ -672,7 +672,7 @@ msgid "Product group"
msgstr "产品组"
msgid "Are you sure you want to delete product group \"%s\"?"
msgstr ""
msgstr "您确定要删除产品组“%s”吗"
msgid "Stay logged in permanently"
msgstr "保持登陆状态"
@ -995,7 +995,7 @@ msgid "Create shopping list"
msgstr "新建购物清单"
msgid "Are you sure you want to delete shopping list \"%s\"?"
msgstr ""
msgstr "您确定要删除购物清单“%s”吗"
msgid "Average shelf life"
msgstr "平均寿命"
@ -1581,7 +1581,7 @@ msgid "Frozen"
msgstr "冷冻"
msgid "Are you sure you want to delete userentity \"%s\"?"
msgstr ""
msgstr "您确定要删除用户实体“%s”吗"
msgid "Shopping list settings"
msgstr "购物清单设置"
@ -1677,7 +1677,7 @@ msgid ""
"Based on the prices of the default consume rule (Opened first, then first "
"due first, then first in first out) for in stock ingredients and on the last"
" price for missing ones"
msgstr ""
msgstr "根据默认消耗规则(先开封,然后先到期,然后先进先出)的库存配料价格和缺少配料的最后价格计算"
msgid "Clear filter"
msgstr "清除筛选"
@ -1839,7 +1839,7 @@ msgid ""
msgstr "当采购产品的到期日期早于库存产品的下次到期日期时显示警告"
msgid "This is due earlier than already in stock items"
msgstr ""
msgstr "此商品比库存中的商品更早到期"
msgid ""
"When enabled, after changing/scanning a product and if all fields could be "
@ -1864,7 +1864,7 @@ msgid "Copy"
msgstr "复制"
msgid "Are you sure you want to remove this barcode?"
msgstr ""
msgstr "您确定要移除此条形码吗?"
msgid "Due date type"
msgstr "到期日期类型"
@ -2029,7 +2029,7 @@ msgid "Reset"
msgstr "重置"
msgid "Are you sure you want to reset the table options?"
msgstr ""
msgstr "您确定要重置表格选项吗?"
msgid "Hide/view columns"
msgstr "显示/隐藏 列"
@ -2093,7 +2093,7 @@ msgid ""
"The stock overview page lists all products which are currently in stock or "
"below their min. stock amount - enable this to hide this product there "
"always"
msgstr ""
msgstr "库存概览页面列出了所有当前有货或低于最低库存量的产品 - 启用此项可始终在此处隐藏该产品"
msgid "Print options"
msgstr "打印选项"
@ -2164,7 +2164,7 @@ msgid "Only done items"
msgstr "‎仅完成项目‎"
msgid "Show only in stock products"
msgstr ""
msgstr "仅显示有库存的产品"
msgid "Product description"
msgstr "产品描述"
@ -2186,7 +2186,7 @@ msgid "When enabled, then this field must be filled on the destination form"
msgstr "‎启用后,必须在目标表单上填写此字段‎"
msgid "In stock products"
msgstr ""
msgstr "库存产品"
msgid "Timestamp"
msgstr "‎时间戳‎"
@ -2265,13 +2265,13 @@ msgid "Edit meal plan section"
msgstr "编辑饮食计划时段"
msgid "Are you sure you want to delete meal plan section \"%s\"?"
msgstr ""
msgstr "您确定要删除用餐计划部分“%s”吗"
msgid "Section"
msgstr "‎时段"
msgid "Are you sure you want to empty the shopping list?"
msgstr ""
msgstr "您确定要清空购物清单吗?"
msgid "This is the default which will be prefilled on purchase"
msgstr "在采购页面预先填入的默认值"
@ -2537,7 +2537,7 @@ msgid "Stock report"
msgstr "库存报表"
msgid "Out of stock products"
msgstr ""
msgstr "缺货产品"
msgid "Quantity unit for prices"
msgstr "价格数量单位"
@ -2552,7 +2552,7 @@ msgid_plural "This means %1$s labels will be printed"
msgstr[0] "%1$s个标签将被打印"
msgid "External barcode lookup"
msgstr ""
msgstr "外部条码查询"
msgid "Error while executing the barcode lookup plugin"
msgstr "运行条码查询插件时出错"
@ -2564,48 +2564,60 @@ msgid "Configure colors"
msgstr "配置颜色"
msgid "Swap track next schedule / track now buttons"
msgstr ""
msgstr "交换“跟踪下一个计划”/“立即跟踪”按钮"
msgid "Scheduled tracking time"
msgstr ""
msgstr "计划跟踪时间"
msgid "Time of tracking"
msgstr ""
msgstr "跟踪时间"
msgid "Show all out of stock products"
msgstr ""
msgstr "显示所有缺货产品"
msgid ""
"By default the stock overview page lists all products which are currently in"
" stock or below their min. stock amount - when this is enabled, all (active)"
" products are always shown"
msgstr ""
msgstr "默认情况下,库存概览页面列出所有当前有库存或低于最低库存量的产品 - 启用此选项后,将始终显示所有(活动的)产品"
msgid "No price information is available for at least one ingredient"
msgstr ""
msgstr "至少有一种配料没有价格信息"
msgid ""
"For ingredients that are only partially in stock, the in stock amount will "
"be consumed."
msgstr ""
msgstr "对于部分有库存的配料,将消耗库存数量。"
msgid "Can't be opened"
msgstr ""
msgstr "无法开封"
msgid "Default purchase price type"
msgstr ""
msgstr "默认购买价格类型"
msgid "This will be used as the default price type selection on purchase"
msgstr ""
msgstr "这将用作购买时的默认价格类型选择"
msgid "Unspecified"
msgstr ""
msgstr "未指定"
msgid "Round up quantity amounts to the nearest whole number"
msgstr ""
msgstr "将数量四舍五入到最接近的整数"
msgid "Stock actions"
msgstr ""
msgstr "库存操作"
msgid "List actions"
msgstr ""
msgstr "列表操作"
msgid "Reports"
msgstr "报表"
msgid "Last Used"
msgstr "上次使用"
msgid "Last Used Report"
msgstr "闲置物品报告"
msgid "Amount in stock"
msgstr "库存数量"

35
migrations/0255.sql Normal file
View File

@ -0,0 +1,35 @@
-- Add common Chinese quantity units
INSERT INTO quantity_units (name, name_plural, description) VALUES ('', '', 'gram');
INSERT INTO quantity_units (name, name_plural, description) VALUES ('', '', 'jin (500g)');
INSERT INTO quantity_units (name, name_plural, description) VALUES ('公斤', '公斤', 'kilogram');
INSERT INTO quantity_units (name, name_plural, description) VALUES ('', '', 'piece/item');
INSERT INTO quantity_units (name, name_plural, description) VALUES ('', '', 'pack');
INSERT INTO quantity_units (name, name_plural, description) VALUES ('', '', 'bottle');
INSERT INTO quantity_units (name, name_plural, description) VALUES ('', '', 'can/jar');
INSERT INTO quantity_units (name, name_plural, description) VALUES ('', '', 'liter');
INSERT INTO quantity_units (name, name_plural, description) VALUES ('毫升', '毫升', 'milliliter');
-- Add conversions between them
-- 1 jin = 500 gram
INSERT INTO quantity_unit_conversions (from_qu_id, to_qu_id, factor)
SELECT from_qu.id, to_qu.id, 500
FROM quantity_units from_qu, quantity_units to_qu
WHERE from_qu.name = '' AND to_qu.name = '';
-- 1 kilogram = 1000 gram
INSERT INTO quantity_unit_conversions (from_qu_id, to_qu_id, factor)
SELECT from_qu.id, to_qu.id, 1000
FROM quantity_units from_qu, quantity_units to_qu
WHERE from_qu.name = '公斤' AND to_qu.name = '';
-- 1 kilogram = 2 jin
INSERT INTO quantity_unit_conversions (from_qu_id, to_qu_id, factor)
SELECT from_qu.id, to_qu.id, 2
FROM quantity_units from_qu, quantity_units to_qu
WHERE from_qu.name = '公斤' AND to_qu.name = '';
-- 1 liter = 1000 milliliter
INSERT INTO quantity_unit_conversions (from_qu_id, to_qu_id, factor)
SELECT from_qu.id, to_qu.id, 1000
FROM quantity_units from_qu, quantity_units to_qu
WHERE from_qu.name = '' AND to_qu.name = '毫升';

View File

@ -0,0 +1,20 @@
var stockLastUsedTable = $('#stock-last-used-table').DataTable({
'order': [[2, 'asc']], // Order by 'Last used' date by default
'columnDefs': [
{ 'orderable': false, 'targets': 0 },
{ 'searchable': false, "targets": 0 }
].concat($.fn.dataTable.defaults.columnDefs)
});
$('#stock-last-used-table tbody').removeClass("d-none");
stockLastUsedTable.columns.adjust().draw();
$("#search").on("keyup", Delay(function()
{
var value = $(this).val();
if (value === "all")
{
value = "";
}
stockLastUsedTable.search(value).draw();
}, Grocy.FormFocusDelay));

View File

@ -64,6 +64,7 @@ $app->group('', function (RouteCollectorProxy $group)
$group->get('/stockentry/{entryId}/label', '\Grocy\Controllers\StockController:StockEntryGrocycodeLabel');
$group->get('/quantityunitconversionsresolved', '\Grocy\Controllers\StockController:QuantityUnitConversionsResolved');
$group->get('/stockreports/spendings', '\Grocy\Controllers\StockReportsController:Spendings');
$group->get('/stockreports/lastused', '\Grocy\Controllers\StockReportsController:LastUsed');
// Stock price tracking
$group->get('/shoppinglocations', '\Grocy\Controllers\StockController:ShoppingLocationsList');

View File

@ -273,6 +273,34 @@
</li>
@endif
<div class="nav-item-divider"></div>
<li class="nav-item nav-item-sidebar"
data-toggle="tooltip"
data-placement="right"
title="{{ $__t('Reports') }}">
<a class="nav-link nav-link-collapse discrete-link collapsed"
data-toggle="collapse"
href="#sub-nav-reports">
<i class="fa-solid fa-fw fa-chart-line"></i>
<span class="nav-link-text">{{ $__t('Reports') }}</span>
</a>
<ul id="sub-nav-reports"
class="sidenav-second-level collapse">
<li>
<a class="nav-link discrete-link"
href="{{ $U('/stockreports/spendings') }}">
<span class="nav-link-text">{{ $__t('Spendings') }}</span>
</a>
</li>
<li>
<a class="nav-link discrete-link"
href="{{ $U('/stockreports/lastused') }}">
<span class="nav-link-text">{{ $__t('Last Used') }}</span>
</a>
</li>
</ul>
</li>
@if(GROCY_FEATURE_FLAG_STOCK)
<div class="nav-item-divider"></div>
<li class="nav-item nav-item-sidebar permission-STOCK_PURCHASE @if($viewName == 'purchase') active-page @endif"

View File

@ -0,0 +1,50 @@
@extends('layout.default')
@section('title', $__t('Last Used Report'))
@section('viewJsName', 'stockreportlastused')
@section('content')
<div class="row">
<div class="col">
<h2 class="title">@yield('title')</h2>
</div>
</div>
<hr class="my-2">
<div class="row">
<div class="col-xs-12 col-md-8 pb-3">
<div class="table-responsive">
<table id="stock-last-used-table" class="table table-striped dt-responsive">
<thead>
<tr>
<th>{{ $__t('Product') }}</th>
<th>{{ $__t('Amount in stock') }}</th>
<th>{{ $__t('Last used') }}</th>
</tr>
</thead>
<tbody class="d-none">
@foreach($products as $product)
<tr>
<td>
{{ $product->name }}
</td>
<td>
{{ $product->amount }}
</td>
<td>
@if($product->last_used)
{{ $product->last_used }}
<time class="timeago timeago-contextual" datetime="{{ $product->last_used }}"></time>
@else
{{ $__t('Never') }}
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
@endsection