diff --git a/db/changes/10270-wisemen/00-ACL.sql b/db/changes/10270-wisemen/00-ACL.sql
new file mode 100644
index 0000000000..40e47b1a35
--- /dev/null
+++ b/db/changes/10270-wisemen/00-ACL.sql
@@ -0,0 +1 @@
+INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) VALUES ('FixedPrice', '*', '*', 'ALLOW', 'ROLE', 'buyer');
\ No newline at end of file
diff --git a/modules/item/back/methods/fixed-price/filter.js b/modules/item/back/methods/fixed-price/filter.js
new file mode 100644
index 0000000000..22cf2bf446
--- /dev/null
+++ b/modules/item/back/methods/fixed-price/filter.js
@@ -0,0 +1,192 @@
+
+const ParameterizedSQL = require('loopback-connector').ParameterizedSQL;
+const buildFilter = require('vn-loopback/util/filter').buildFilter;
+const mergeFilters = require('vn-loopback/util/filter').mergeFilters;
+
+module.exports = Self => {
+ Self.remoteMethodCtx('filter', {
+ description: 'Find all instances of the model matched by filter from the data source.',
+ accessType: 'READ',
+ accepts: [
+ {
+ arg: 'filter',
+ type: 'object',
+ description: 'Filter defining where, order, offset, and limit - must be a JSON-encoded string',
+ },
+ {
+ arg: 'search',
+ type: 'string',
+ description: `If it's and integer searchs by itemFk, otherwise it searchs by the itemType code`,
+ },
+ {
+ arg: 'itemFk',
+ type: 'integer',
+ description: 'The item id',
+ },
+ {
+ arg: 'typeFk',
+ type: 'integer',
+ description: 'The item type id',
+ },
+ {
+ arg: 'categoryFk',
+ type: 'integer',
+ description: 'The item category id',
+ },
+ {
+ arg: 'warehouseFk',
+ type: 'integer',
+ description: 'The warehouse id',
+ },
+ {
+ arg: 'buyerFk',
+ type: 'integer',
+ description: 'The buyer id',
+ },
+ {
+ arg: 'rate2',
+ type: 'integer',
+ description: 'The price per unit',
+ },
+ {
+ arg: 'rate3',
+ type: 'integer',
+ description: 'The price per package',
+ },
+ {
+ arg: 'minPrice',
+ type: 'integer',
+ description: 'The minimum price of the item',
+ },
+ {
+ arg: 'hasMinPrice',
+ type: 'boolean',
+ description: 'whether a minimum price has been defined for the item',
+ },
+ {
+ arg: 'started',
+ type: 'date',
+ description: 'Price validity start date',
+ },
+ {
+ arg: 'ended',
+ type: 'date',
+ description: 'Price validity end date',
+ },
+ {
+ arg: 'tags',
+ type: ['object'],
+ description: 'List of tags to filter with',
+ },
+ {
+ arg: 'mine',
+ type: 'Boolean',
+ description: `Search requests attended by the current user`
+ }
+ ],
+ returns: {
+ type: ['Object'],
+ root: true
+ },
+ http: {
+ path: `/filter`,
+ verb: 'GET'
+ }
+ });
+
+ Self.filter = async(ctx, filter) => {
+ const conn = Self.dataSource.connector;
+ let userId = ctx.req.accessToken.userId;
+
+ if (ctx.args.mine)
+ ctx.args.buyerFk = userId;
+
+ const where = buildFilter(ctx.args, (param, value) => {
+ switch (param) {
+ case 'search':
+ return /^\d+$/.test(value)
+ ? {'fp.itemFk': {inq: value}}
+ : {'it.code': {like: `%${value}%`}};
+ case 'categoryFk':
+ return {'it.categoryFk': value};
+ case 'buyerFk':
+ return {'it.workerFk': value};
+ case 'warehouseFk':
+ case 'rate2':
+ case 'rate3':
+ case 'started':
+ case 'ended':
+ param = `fp.${param}`;
+ return {[param]: value};
+ case 'minPrice':
+ case 'hasMinPrice':
+ case 'typeFk':
+ param = `i.${param}`;
+ return {[param]: value};
+ }
+ });
+ filter = mergeFilters(filter, {where});
+
+ const stmts = [];
+ let stmt;
+
+ stmt = new ParameterizedSQL(
+ `SELECT fp.id,
+ fp.itemFk,
+ fp.warehouseFk,
+ fp.rate2,
+ fp.rate3,
+ fp.started,
+ fp.ended,
+ i.minPrice,
+ i.hasMinPrice,
+ i.name,
+ i.subName,
+ i.tag5,
+ i.value5,
+ i.tag6,
+ i.value6,
+ i.tag7,
+ i.value7,
+ i.tag8,
+ i.value8,
+ i.tag9,
+ i.value9,
+ i.tag10,
+ i.value10
+ FROM priceFixed fp
+ JOIN item i ON i.id = fp.itemFk
+ JOIN itemType it ON it.id = i.typeFk`
+ );
+
+ if (ctx.args.tags) {
+ let i = 1;
+ for (const tag of ctx.args.tags) {
+ const tAlias = `it${i++}`;
+
+ if (tag.tagFk) {
+ stmt.merge({
+ sql: `JOIN vn.itemTag ${tAlias} ON ${tAlias}.itemFk = i.id
+ AND ${tAlias}.tagFk = ?
+ AND ${tAlias}.value LIKE ?`,
+ params: [tag.tagFk, `%${tag.value}%`],
+ });
+ } else {
+ stmt.merge({
+ sql: `JOIN vn.itemTag ${tAlias} ON ${tAlias}.itemFk = i.id
+ AND ${tAlias}.value LIKE ?`,
+ params: [`%${tag.value}%`],
+ });
+ }
+ }
+ }
+
+ stmt.merge(conn.makeWhere(filter.where));
+ stmt.merge(conn.makePagination(filter));
+
+ const fixedPriceIndex = stmts.push(stmt) - 1;
+ const sql = ParameterizedSQL.join(stmts, ';');
+ const result = await conn.executeStmt(sql);
+ return fixedPriceIndex === 0 ? result : result[fixedPriceIndex];
+ };
+};
diff --git a/modules/item/back/methods/fixed-price/specs/upsertFixedPrice.spec.js b/modules/item/back/methods/fixed-price/specs/upsertFixedPrice.spec.js
new file mode 100644
index 0000000000..67910d4175
--- /dev/null
+++ b/modules/item/back/methods/fixed-price/specs/upsertFixedPrice.spec.js
@@ -0,0 +1,62 @@
+const app = require('vn-loopback/server/server');
+
+describe('upsertFixedPrice()', () => {
+ const now = new Date();
+ const fixedPriceId = 1;
+ let originalFixedPrice;
+ let originalItem;
+
+ beforeAll(async() => {
+ originalFixedPrice = await app.models.FixedPrice.findById(fixedPriceId);
+ originalItem = await app.models.Item.findById(originalFixedPrice.itemFk);
+ });
+
+ beforeAll(async() => {
+ await originalFixedPrice.save();
+ await originalItem.save();
+ });
+
+ it(`should toggle the hasMinPrice boolean if there's a minPrice and update the rest of the data`, async() => {
+ const ctx = {args: {
+ id: fixedPriceId,
+ itemFk: 1,
+ warehouseFk: 1,
+ rate2: 100,
+ rate3: 300,
+ started: now,
+ ended: now,
+ minPrice: 100,
+ hasMinPrice: false
+ }};
+
+ const result = await app.models.FixedPrice.upsertFixedPrice(ctx, ctx.args.id);
+
+ delete ctx.args.started;
+ delete ctx.args.ended;
+ ctx.args.hasMinPrice = true;
+
+ expect(result).toEqual(jasmine.objectContaining(ctx.args));
+ });
+
+ it(`should toggle the hasMinPrice boolean if there's no minPrice and update the rest of the data`, async() => {
+ const ctx = {args: {
+ id: fixedPriceId,
+ itemFk: 1,
+ warehouseFk: 1,
+ rate2: 2.5,
+ rate3: 2,
+ started: now,
+ ended: now,
+ minPrice: 0,
+ hasMinPrice: true
+ }};
+
+ const result = await app.models.FixedPrice.upsertFixedPrice(ctx, ctx.args.id);
+
+ delete ctx.args.started;
+ delete ctx.args.ended;
+ ctx.args.hasMinPrice = false;
+
+ expect(result).toEqual(jasmine.objectContaining(ctx.args));
+ });
+});
diff --git a/modules/item/back/methods/fixed-price/upsertFixedPrice.js b/modules/item/back/methods/fixed-price/upsertFixedPrice.js
new file mode 100644
index 0000000000..dbdeebdab1
--- /dev/null
+++ b/modules/item/back/methods/fixed-price/upsertFixedPrice.js
@@ -0,0 +1,116 @@
+module.exports = Self => {
+ Self.remoteMethod('upsertFixedPrice', {
+ description: 'Inserts or updates a fixed price for an item',
+ accessType: 'WRITE',
+ accepts: [{
+ arg: 'ctx',
+ type: 'object',
+ http: {source: 'context'}
+ },
+ {
+ arg: 'id',
+ type: 'number',
+ description: 'The fixed price id'
+ },
+ {
+ arg: 'itemFk',
+ type: 'number'
+ },
+ {
+ arg: 'warehouseFk',
+ type: 'number'
+ },
+ {
+ arg: 'started',
+ type: 'date'
+ },
+ {
+ arg: 'ended',
+ type: 'date'
+ },
+ {
+ arg: 'rate2',
+ type: 'number'
+ },
+ {
+ arg: 'rate3',
+ type: 'number'
+ },
+ {
+ arg: 'minPrice',
+ type: 'number'
+ },
+ {
+ arg: 'hasMinPrice',
+ type: 'any'
+ }],
+ returns: {
+ type: 'object',
+ root: true
+ },
+ http: {
+ path: `/upsertFixedPrice`,
+ verb: 'PATCH'
+ }
+ });
+
+ Self.upsertFixedPrice = async ctx => {
+ const models = Self.app.models;
+
+ const args = ctx.args;
+ const tx = await models.Address.beginTransaction({});
+ try {
+ const options = {transaction: tx};
+ delete args.ctx; // removed unwanted data
+
+ const fixedPrice = await models.FixedPrice.upsert(args, options);
+ const targetItem = await models.Item.findById(args.itemFk, null, options);
+ await targetItem.updateAttributes({
+ minPrice: args.minPrice,
+ hasMinPrice: args.minPrice ? true : false
+ }, options);
+
+ const itemFields = [
+ 'minPrice',
+ 'hasMinPrice',
+ 'name',
+ 'subName',
+ 'tag5',
+ 'value5',
+ 'tag6',
+ 'value6',
+ 'tag7',
+ 'value7',
+ 'tag8',
+ 'value8',
+ 'tag9',
+ 'value9',
+ 'tag10',
+ 'value10'
+ ];
+
+ const fieldsCopy = [].concat(itemFields);
+ const filter = {
+ include: {
+ relation: 'item',
+ scope: {
+ fields: fieldsCopy
+ }
+ }
+ };
+
+ const result = await models.FixedPrice.findById(fixedPrice.id, filter, options);
+ const item = result.item();
+
+ for (let key of itemFields)
+ result[key] = item[key];
+
+ await tx.commit();
+ return result;
+ } catch (e) {
+ await tx.rollback();
+ throw e;
+ }
+ };
+};
+
diff --git a/modules/item/back/model-config.json b/modules/item/back/model-config.json
index 13e30dc156..9f101f9c7a 100644
--- a/modules/item/back/model-config.json
+++ b/modules/item/back/model-config.json
@@ -76,5 +76,8 @@
},
"TaxType": {
"dataSource": "vn"
+ },
+ "FixedPrice": {
+ "dataSource": "vn"
}
}
diff --git a/modules/item/back/models/fixed-price.js b/modules/item/back/models/fixed-price.js
new file mode 100644
index 0000000000..9c78c586f4
--- /dev/null
+++ b/modules/item/back/models/fixed-price.js
@@ -0,0 +1,4 @@
+module.exports = Self => {
+ require('../methods/fixed-price/filter')(Self);
+ require('../methods/fixed-price/upsertFixedPrice')(Self);
+};
diff --git a/modules/item/back/models/fixed-price.json b/modules/item/back/models/fixed-price.json
new file mode 100644
index 0000000000..85e9194a3b
--- /dev/null
+++ b/modules/item/back/models/fixed-price.json
@@ -0,0 +1,59 @@
+{
+ "name": "FixedPrice",
+ "base": "VnModel",
+ "options": {
+ "mysql": {
+ "table": "priceFixed"
+ }
+ },
+ "properties": {
+ "id": {
+ "type": "number",
+ "id": true,
+ "description": "Identifier"
+ },
+ "itemFk": {
+ "type": "number",
+ "required": true
+ },
+ "warehouseFk": {
+ "type": "number"
+ },
+ "rate2": {
+ "type": "number",
+ "required": true
+ },
+ "rate3": {
+ "type": "number",
+ "required": true
+ },
+ "started": {
+ "type": "date",
+ "required": true
+ },
+ "ended": {
+ "type": "date",
+ "required": true
+ }
+ },
+ "relations": {
+ "item": {
+ "type": "belongsTo",
+ "model": "Item",
+ "foreignKey": "itemFk"
+ },
+ "warehouse": {
+ "type": "belongsTo",
+ "model": "Warehouse",
+ "foreignKey": "warehouseFk"
+ }
+ },
+ "acls": [
+ {
+ "accessType": "READ",
+ "principalType": "ROLE",
+ "principalId": "buyer",
+ "permission": "ALLOW"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/modules/item/front/fixed-price-search-panel/index.html b/modules/item/front/fixed-price-search-panel/index.html
new file mode 100644
index 0000000000..5a1e7781e3
--- /dev/null
+++ b/modules/item/front/fixed-price-search-panel/index.html
@@ -0,0 +1,136 @@
+