@odooconnector/medusa-odoo-connector 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.medusa/server/src/admin/index.js +2211 -0
- package/.medusa/server/src/admin/index.mjs +2212 -0
- package/.medusa/server/src/api/admin/odoo/billing/route.js +15 -0
- package/.medusa/server/src/api/admin/odoo/clear-orders/route.js +33 -0
- package/.medusa/server/src/api/admin/odoo/clear-products/route.js +30 -0
- package/.medusa/server/src/api/admin/odoo/config/route.js +44 -0
- package/.medusa/server/src/api/admin/odoo/customer-options/route.js +15 -0
- package/.medusa/server/src/api/admin/odoo/inventory-options/route.js +15 -0
- package/.medusa/server/src/api/admin/odoo/jobs/[id]/route.js +25 -0
- package/.medusa/server/src/api/admin/odoo/logs/route.js +27 -0
- package/.medusa/server/src/api/admin/odoo/options/route.js +15 -0
- package/.medusa/server/src/api/admin/odoo/order-options/route.js +15 -0
- package/.medusa/server/src/api/admin/odoo/product-options/route.js +15 -0
- package/.medusa/server/src/api/admin/odoo/sections/[section]/route.js +55 -0
- package/.medusa/server/src/api/admin/odoo/settings/route.js +91 -0
- package/.medusa/server/src/api/admin/odoo/stripe/change/route.js +20 -0
- package/.medusa/server/src/api/admin/odoo/stripe/checkout/route.js +20 -0
- package/.medusa/server/src/api/admin/odoo/stripe/portal/route.js +16 -0
- package/.medusa/server/src/api/admin/odoo/sync/[entity]/route.js +99 -0
- package/.medusa/server/src/api/admin/odoo/sync-order/[id]/route.js +17 -0
- package/.medusa/server/src/api/admin/odoo/verify-connection/route.js +39 -0
- package/.medusa/server/src/api/odoo/billing/sync/route.js +42 -0
- package/.medusa/server/src/index.js +3 -0
- package/.medusa/server/src/jobs/import-worker.js +61 -0
- package/.medusa/server/src/lib/export-products.js +141 -0
- package/.medusa/server/src/lib/import-products.js +304 -0
- package/.medusa/server/src/lib/odoo-api-client.js +32 -0
- package/.medusa/server/src/lib/plan.js +83 -0
- package/.medusa/server/src/lib/queue.js +79 -0
- package/.medusa/server/src/lib/sync-customers.js +183 -0
- package/.medusa/server/src/lib/sync-inventory.js +135 -0
- package/.medusa/server/src/lib/sync-order.js +134 -0
- package/.medusa/server/src/lib/sync-products.js +32 -0
- package/.medusa/server/src/modules/odoo/index.js +13 -0
- package/.medusa/server/src/modules/odoo/migrations/Migration20260619101616.js +17 -0
- package/.medusa/server/src/modules/odoo/models/odoo-setting.js +19 -0
- package/.medusa/server/src/modules/odoo/service.js +28 -0
- package/.medusa/server/src/subscribers/order-placed.js +24 -0
- package/.medusa/server/src/workflows/index.js +3 -0
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/package.json +81 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.syncOrder = syncOrder;
|
|
4
|
+
const utils_1 = require("@medusajs/framework/utils");
|
|
5
|
+
const odoo_1 = require("../modules/odoo");
|
|
6
|
+
const odoo_api_client_1 = require("./odoo-api-client");
|
|
7
|
+
const plan_1 = require("./plan");
|
|
8
|
+
const PAID = ["captured", "partially_captured", "paid", "partially_paid"];
|
|
9
|
+
const FULFILLED = ["fulfilled", "partially_fulfilled", "delivered", "partially_delivered"];
|
|
10
|
+
/**
|
|
11
|
+
* Pushes a single Medusa order to Odoo as a sale order (+ optional invoice /
|
|
12
|
+
* payment). Idempotent via order.metadata.odoo_order_id. Medusa gathers + maps;
|
|
13
|
+
* odoo-api does the Odoo writes.
|
|
14
|
+
*/
|
|
15
|
+
async function syncOrder(scope, orderId, opts = {}) {
|
|
16
|
+
const svc = scope.resolve(odoo_1.ODOO_MODULE);
|
|
17
|
+
const query = scope.resolve(utils_1.ContainerRegistrationKeys.QUERY);
|
|
18
|
+
const logSkip = async (message) => {
|
|
19
|
+
try {
|
|
20
|
+
await (0, odoo_api_client_1.odooApi)("/odoo/logs", {
|
|
21
|
+
method: "POST",
|
|
22
|
+
body: JSON.stringify({ type: "order_sync", status: "skipped", message }),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
/* non-critical */
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const settings = ((await svc.getValue("section:orders")) ?? {});
|
|
30
|
+
// The auto-sync toggle gates background syncs; a manual "Sync now" bypasses it
|
|
31
|
+
// (the plan gate below still applies).
|
|
32
|
+
if (!settings.orderSync && !opts.manual) {
|
|
33
|
+
await logSkip(`Order ${orderId}: skipped — order sync is disabled.`);
|
|
34
|
+
return { ok: true, skipped: true, message: "Order sync is disabled." };
|
|
35
|
+
}
|
|
36
|
+
const plan = await (0, plan_1.getPlanLimits)(scope);
|
|
37
|
+
if (!plan.entities.orders) {
|
|
38
|
+
const message = `Order sync requires the Starter plan or higher (current: ${plan.name}).`;
|
|
39
|
+
await logSkip(`Order ${orderId}: skipped — ${message}`);
|
|
40
|
+
return { ok: false, skipped: true, message };
|
|
41
|
+
}
|
|
42
|
+
const config = ((await svc.getValue("config")) ?? {});
|
|
43
|
+
const { data } = await query.graph({
|
|
44
|
+
entity: "order",
|
|
45
|
+
fields: [
|
|
46
|
+
"id",
|
|
47
|
+
"email",
|
|
48
|
+
"currency_code",
|
|
49
|
+
"payment_status",
|
|
50
|
+
"fulfillment_status",
|
|
51
|
+
"metadata",
|
|
52
|
+
"customer.first_name",
|
|
53
|
+
"customer.last_name",
|
|
54
|
+
"customer.email",
|
|
55
|
+
"customer.phone",
|
|
56
|
+
"items.title",
|
|
57
|
+
"items.quantity",
|
|
58
|
+
"items.unit_price",
|
|
59
|
+
"items.variant_sku",
|
|
60
|
+
],
|
|
61
|
+
filters: { id: orderId },
|
|
62
|
+
});
|
|
63
|
+
const order = data[0];
|
|
64
|
+
if (!order)
|
|
65
|
+
return { ok: false, message: `Order ${orderId} not found.` };
|
|
66
|
+
// Idempotency.
|
|
67
|
+
if (order.metadata?.odoo_order_id) {
|
|
68
|
+
const odooId = order.metadata.odoo_order_id;
|
|
69
|
+
await logSkip(`Order ${order.id}: skipped — already synced (Odoo order ${odooId}).`);
|
|
70
|
+
return {
|
|
71
|
+
ok: true,
|
|
72
|
+
skipped: true,
|
|
73
|
+
message: `Already synced (Odoo order ${odooId}).`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const paid = PAID.includes(order.payment_status);
|
|
77
|
+
const fulfilled = FULFILLED.includes(order.fulfillment_status);
|
|
78
|
+
const shouldInvoice = !!settings.exportInvoice &&
|
|
79
|
+
(!settings.invoiceMode ||
|
|
80
|
+
settings.invoiceMode === "all" ||
|
|
81
|
+
(settings.invoiceMode === "paid" && paid) ||
|
|
82
|
+
(settings.invoiceMode === "fulfilled" && fulfilled));
|
|
83
|
+
const name = [order.customer?.first_name, order.customer?.last_name].filter(Boolean).join(" ") ||
|
|
84
|
+
order.email;
|
|
85
|
+
const payload = {
|
|
86
|
+
externalId: order.id,
|
|
87
|
+
customer: {
|
|
88
|
+
name,
|
|
89
|
+
email: order.customer?.email || order.email,
|
|
90
|
+
phone: order.customer?.phone,
|
|
91
|
+
},
|
|
92
|
+
companyId: config.company_id ?? null,
|
|
93
|
+
currencyCode: order.currency_code,
|
|
94
|
+
lines: (order.items || []).map((it) => ({
|
|
95
|
+
sku: it.variant_sku || "",
|
|
96
|
+
name: it.title,
|
|
97
|
+
qty: it.quantity,
|
|
98
|
+
price: it.unit_price,
|
|
99
|
+
})),
|
|
100
|
+
exportInvoice: shouldInvoice,
|
|
101
|
+
invoiceMode: settings.invoiceMode,
|
|
102
|
+
markInvoicePaid: !!settings.markInvoicePaid,
|
|
103
|
+
createPayment: !!settings.createPayment,
|
|
104
|
+
paymentJournalId: settings.paymentJournalId ?? null,
|
|
105
|
+
};
|
|
106
|
+
let result;
|
|
107
|
+
try {
|
|
108
|
+
result = await (0, odoo_api_client_1.odooApi)("/odoo/orders", { method: "POST", body: JSON.stringify(payload) });
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
result = { ok: false, message: e?.message ?? "Order sync failed" };
|
|
112
|
+
}
|
|
113
|
+
if (result.ok && result.odooOrderId) {
|
|
114
|
+
const orderModule = scope.resolve(utils_1.Modules.ORDER);
|
|
115
|
+
await orderModule.updateOrders([
|
|
116
|
+
{ id: order.id, metadata: { ...(order.metadata || {}), odoo_order_id: result.odooOrderId } },
|
|
117
|
+
]);
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
await (0, odoo_api_client_1.odooApi)("/odoo/logs", {
|
|
121
|
+
method: "POST",
|
|
122
|
+
body: JSON.stringify({
|
|
123
|
+
type: "order_sync",
|
|
124
|
+
status: result.ok ? "success" : "failed",
|
|
125
|
+
message: `Order ${order.id}: ${result.message}`,
|
|
126
|
+
}),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
/* non-critical */
|
|
131
|
+
}
|
|
132
|
+
return { ok: !!result.ok, odooOrderId: result.odooOrderId, message: result.message };
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3luYy1vcmRlci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3NyYy9saWIvc3luYy1vcmRlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQStCQSw4QkFvSUM7QUFsS0QscURBQThFO0FBQzlFLDBDQUE2QztBQUU3Qyx1REFBMkM7QUFDM0MsaUNBQXNDO0FBa0J0QyxNQUFNLElBQUksR0FBRyxDQUFDLFVBQVUsRUFBRSxvQkFBb0IsRUFBRSxNQUFNLEVBQUUsZ0JBQWdCLENBQUMsQ0FBQTtBQUN6RSxNQUFNLFNBQVMsR0FBRyxDQUFDLFdBQVcsRUFBRSxxQkFBcUIsRUFBRSxXQUFXLEVBQUUscUJBQXFCLENBQUMsQ0FBQTtBQUUxRjs7OztHQUlHO0FBQ0ksS0FBSyxVQUFVLFNBQVMsQ0FDN0IsS0FBc0IsRUFDdEIsT0FBZSxFQUNmLE9BQTZCLEVBQUU7SUFFL0IsTUFBTSxHQUFHLEdBQUcsS0FBSyxDQUFDLE9BQU8sQ0FBcUIsa0JBQVcsQ0FBQyxDQUFBO0lBQzFELE1BQU0sS0FBSyxHQUFHLEtBQUssQ0FBQyxPQUFPLENBQUMsaUNBQXlCLENBQUMsS0FBSyxDQUFDLENBQUE7SUFFNUQsTUFBTSxPQUFPLEdBQUcsS0FBSyxFQUFFLE9BQWUsRUFBRSxFQUFFO1FBQ3hDLElBQUksQ0FBQztZQUNILE1BQU0sSUFBQSx5QkFBTyxFQUFDLFlBQVksRUFBRTtnQkFDMUIsTUFBTSxFQUFFLE1BQU07Z0JBQ2QsSUFBSSxFQUFFLElBQUksQ0FBQyxTQUFTLENBQUMsRUFBRSxJQUFJLEVBQUUsWUFBWSxFQUFFLE1BQU0sRUFBRSxTQUFTLEVBQUUsT0FBTyxFQUFFLENBQUM7YUFDekUsQ0FBQyxDQUFBO1FBQ0osQ0FBQztRQUFDLE1BQU0sQ0FBQztZQUNQLGtCQUFrQjtRQUNwQixDQUFDO0lBQ0gsQ0FBQyxDQUFBO0lBRUQsTUFBTSxRQUFRLEdBQUcsQ0FBQyxDQUFDLE1BQU0sR0FBRyxDQUFDLFFBQVEsQ0FBQyxnQkFBZ0IsQ0FB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
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.syncProducts = syncProducts;
|
|
4
|
+
const odoo_1 = require("../modules/odoo");
|
|
5
|
+
const import_products_1 = require("./import-products");
|
|
6
|
+
const export_products_1 = require("./export-products");
|
|
7
|
+
const plan_1 = require("./plan");
|
|
8
|
+
const odoo_api_client_1 = require("./odoo-api-client");
|
|
9
|
+
/**
|
|
10
|
+
* Runs the product sync in the direction chosen on the Product Sync page,
|
|
11
|
+
* enforcing the active plan:
|
|
12
|
+
* - medusa_to_odoo → export (push) — requires Starter or higher
|
|
13
|
+
* - odoo_to_medusa → import (default; pull Odoo products into Medusa)
|
|
14
|
+
*/
|
|
15
|
+
async function syncProducts(scope) {
|
|
16
|
+
const svc = scope.resolve(odoo_1.ODOO_MODULE);
|
|
17
|
+
const settings = ((await svc.getValue("section:product")) ?? {});
|
|
18
|
+
if (settings.direction === "medusa_to_odoo") {
|
|
19
|
+
const plan = await (0, plan_1.getPlanLimits)(scope);
|
|
20
|
+
if (!plan.bothDirections) {
|
|
21
|
+
const message = `Export (Medusa → Odoo) is not available on the ${plan.name} plan. Upgrade to Starter or higher to push products to Odoo.`;
|
|
22
|
+
await (0, odoo_api_client_1.odooApi)("/odoo/logs", {
|
|
23
|
+
method: "POST",
|
|
24
|
+
body: JSON.stringify({ type: "product_sync", status: "skipped", message }),
|
|
25
|
+
}).catch(() => { });
|
|
26
|
+
return { ok: false, created: 0, updated: 0, skipped: 0, failed: 0, message };
|
|
27
|
+
}
|
|
28
|
+
return (0, export_products_1.exportProducts)(scope);
|
|
29
|
+
}
|
|
30
|
+
return (0, import_products_1.importProducts)(scope);
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3luYy1wcm9kdWN0cy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3NyYy9saWIvc3luYy1wcm9kdWN0cy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQWNBLG9DQWlCQztBQTlCRCwwQ0FBNkM7QUFFN0MsdURBQWtEO0FBQ2xELHVEQUFrRDtBQUNsRCxpQ0FBc0M7QUFDdEMsdURBQTJDO0FBRTNDOzs7OztHQUtHO0FBQ0ksS0FBSyxVQUFVLFlBQVksQ0FBQyxLQUFzQjtJQUN2RCxNQUFNLEdBQUcsR0FBRyxLQUFLLENBQUMsT0FBTyxDQUFxQixrQkFBVyxDQUFDLENBQUE7SUFDMUQsTUFBTSxRQUFRLEdBQUcsQ0FBQyxDQUFDLE1BQU0sR0FBRyxDQUFDLFFBQVEsQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDLElBQUksRUFBRSxDQUEyQixDQUFBO0lBRTFGLElBQUksUUFBUSxDQUFDLFNBQVMsS0FBSyxnQkFBZ0IsRUFBRSxDQUFDO1FBQzVDLE1BQU0sSUFBSSxHQUFHLE1BQU0sSUFBQSxvQkFBYSxFQUFDLEtBQUssQ0FBQyxDQUFBO1FBQ3ZDLElBQUksQ0FBQyxJQUFJLENBQUMsY0FBYyxFQUFFLENBQUM7WUFDekIsTUFBTSxPQUFPLEdBQUcsa0RBQWtELElBQUksQ0FBQyxJQUFJLCtEQUErRCxDQUFBO1lBQzFJLE1BQU0sSUFBQSx5QkFBTyxFQUFDLFlBQVksRUFBRTtnQkFDMUIsTUFBTSxFQUFFLE1BQU07Z0JBQ2QsSUFBSSxFQUFFLElBQUksQ0FBQyxTQUFTLENBQUMsRUFBRSxJQUFJLEVBQUUsY0FBYyxFQUFFLE1BQU0sRUFBRSxTQUFTLEVBQUUsT0FBTyxFQUFFLENBQUM7YUFDM0UsQ0FBQyxDQUFDLEtBQUssQ0FBQyxHQUFHLEVBQUUsR0FBRSxDQUFDLENBQUMsQ0FBQTtZQUNsQixPQUFPLEVBQUUsRUFBRSxFQUFFLEtBQUssRUFBRSxPQUFPLEVBQUUsQ0FBQyxFQUFFLE9BQU8sRUFBRSxDQUFDLEVBQUUsT0FBTyxFQUFFLENBQUMsRUFBRSxNQUFNLEVBQUUsQ0FBQyxFQUFFLE9BQU8sRUFBRSxDQUFBO1FBQzlFLENBQUM7UUFDRCxPQUFPLElBQUEsZ0NBQWMsRUFBQyxLQUFLLENBQUMsQ0FBQTtJQUM5QixDQUFDO0lBQ0QsT0FBTyxJQUFBLGdDQUFjLEVBQUMsS0FBSyxDQUFDLENBQUE7QUFDOUIsQ0FBQyJ9
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ODOO_MODULE = void 0;
|
|
7
|
+
const utils_1 = require("@medusajs/framework/utils");
|
|
8
|
+
const service_1 = __importDefault(require("./service"));
|
|
9
|
+
exports.ODOO_MODULE = "odoo";
|
|
10
|
+
exports.default = (0, utils_1.Module)(exports.ODOO_MODULE, {
|
|
11
|
+
service: service_1.default,
|
|
12
|
+
});
|
|
13
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi9zcmMvbW9kdWxlcy9vZG9vL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7OztBQUFBLHFEQUFrRDtBQUNsRCx3REFBMEM7QUFFN0IsUUFBQSxXQUFXLEdBQUcsTUFBTSxDQUFBO0FBRWpDLGtCQUFlLElBQUEsY0FBTSxFQUFDLG1CQUFXLEVBQUU7SUFDakMsT0FBTyxFQUFFLGlCQUFrQjtDQUM1QixDQUFDLENBQUEifQ==
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Migration20260619101616 = void 0;
|
|
4
|
+
const migrations_1 = require("@medusajs/framework/mikro-orm/migrations");
|
|
5
|
+
class Migration20260619101616 extends migrations_1.Migration {
|
|
6
|
+
async up() {
|
|
7
|
+
this.addSql(`alter table if exists "odoo_setting" drop constraint if exists "odoo_setting_key_unique";`);
|
|
8
|
+
this.addSql(`create table if not exists "odoo_setting" ("id" text not null, "key" text not null, "value" jsonb not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "odoo_setting_pkey" primary key ("id"));`);
|
|
9
|
+
this.addSql(`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_odoo_setting_key_unique" ON "odoo_setting" ("key") WHERE deleted_at IS NULL;`);
|
|
10
|
+
this.addSql(`CREATE INDEX IF NOT EXISTS "IDX_odoo_setting_deleted_at" ON "odoo_setting" ("deleted_at") WHERE deleted_at IS NULL;`);
|
|
11
|
+
}
|
|
12
|
+
async down() {
|
|
13
|
+
this.addSql(`drop table if exists "odoo_setting" cascade;`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
exports.Migration20260619101616 = Migration20260619101616;
|
|
17
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiTWlncmF0aW9uMjAyNjA2MTkxMDE2MTYuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi9zcmMvbW9kdWxlcy9vZG9vL21pZ3JhdGlvbnMvTWlncmF0aW9uMjAyNjA2MTkxMDE2MTYudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7O0FBQUEseUVBQXFFO0FBRXJFLE1BQWEsdUJBQXdCLFNBQVEsc0JBQVM7SUFFM0MsS0FBSyxDQUFDLEVBQUU7UUFDZixJQUFJLENBQUMsTUFBTSxDQUFDLDJGQUEyRixDQUFDLENBQUM7UUFDekcsSUFBSSxDQUFDLE1BQU0sQ0FBQyxrU0FBa1MsQ0FBQyxDQUFDO1FBQ2hULElBQUksQ0FBQyxNQUFNLENBQUMscUhBQXFILENBQUMsQ0FBQztRQUNuSSxJQUFJLENBQUMsTUFBTSxDQUFDLHFIQUFxSCxDQUFDLENBQUM7SUFDckksQ0FBQztJQUVRLEtBQUssQ0FBQyxJQUFJO1FBQ2pCLElBQUksQ0FBQyxNQUFNLENBQUMsOENBQThDLENBQUMsQ0FBQztJQUM5RCxDQUFDO0NBRUY7QUFiRCwwREFhQyJ9
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.OdooSetting = void 0;
|
|
4
|
+
const utils_1 = require("@medusajs/framework/utils");
|
|
5
|
+
/**
|
|
6
|
+
* Generic key/value config store. Medusa is the source of truth for all
|
|
7
|
+
* connector configuration; each logical config is one row keyed by `key`:
|
|
8
|
+
* - "connection" → Odoo connection (url, database, username, api_key)
|
|
9
|
+
* - "config" → general configuration
|
|
10
|
+
* - "section:product" → the Product Sync page settings
|
|
11
|
+
* - "section:<name>" → other settings pages
|
|
12
|
+
* The value is stored as JSON. odoo-api keeps a synced copy as a cache.
|
|
13
|
+
*/
|
|
14
|
+
exports.OdooSetting = utils_1.model.define("odoo_setting", {
|
|
15
|
+
id: utils_1.model.id().primaryKey(),
|
|
16
|
+
key: utils_1.model.text().unique(),
|
|
17
|
+
value: utils_1.model.json(),
|
|
18
|
+
});
|
|
19
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib2Rvby1zZXR0aW5nLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vLi4vLi4vc3JjL21vZHVsZXMvb2Rvby9tb2RlbHMvb2Rvby1zZXR0aW5nLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7OztBQUFBLHFEQUFpRDtBQUVqRDs7Ozs7Ozs7R0FRRztBQUNVLFFBQUEsV0FBVyxHQUFHLGFBQUssQ0FBQyxNQUFNLENBQUMsY0FBYyxFQUFFO0lBQ3RELEVBQUUsRUFBRSxhQUFLLENBQUMsRUFBRSxFQUFFLENBQUMsVUFBVSxFQUFFO0lBQzNCLEdBQUcsRUFBRSxhQUFLLENBQUMsSUFBSSxFQUFFLENBQUMsTUFBTSxFQUFFO0lBQzFCLEtBQUssRUFBRSxhQUFLLENBQUMsSUFBSSxFQUFFO0NBQ3BCLENBQUMsQ0FBQSJ9
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const utils_1 = require("@medusajs/framework/utils");
|
|
4
|
+
const odoo_setting_1 = require("./models/odoo-setting");
|
|
5
|
+
/**
|
|
6
|
+
* OdooSettingService — thin key/value accessor over the OdooSetting model.
|
|
7
|
+
*/
|
|
8
|
+
class OdooSettingService extends (0, utils_1.MedusaService)({ OdooSetting: odoo_setting_1.OdooSetting }) {
|
|
9
|
+
/** Returns the stored value for a key, or null. */
|
|
10
|
+
async getValue(key) {
|
|
11
|
+
const [row] = await this.listOdooSettings({ key }, { take: 1 });
|
|
12
|
+
return row?.value ?? null;
|
|
13
|
+
}
|
|
14
|
+
/** Upserts the value for a key. */
|
|
15
|
+
async setValue(key, value) {
|
|
16
|
+
const v = value;
|
|
17
|
+
const [existing] = await this.listOdooSettings({ key }, { take: 1 });
|
|
18
|
+
if (existing) {
|
|
19
|
+
await this.updateOdooSettings({ id: existing.id, value: v });
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
await this.createOdooSettings({ key, value: v });
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
exports.default = OdooSettingService;
|
|
28
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VydmljZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uLy4uL3NyYy9tb2R1bGVzL29kb28vc2VydmljZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUFBLHFEQUF5RDtBQUN6RCx3REFBbUQ7QUFFbkQ7O0dBRUc7QUFDSCxNQUFNLGtCQUFtQixTQUFRLElBQUEscUJBQWEsRUFBQyxFQUFFLFdBQVcsRUFBWCwwQkFBVyxFQUFFLENBQUM7SUFDN0QsbURBQW1EO0lBQ25ELEtBQUssQ0FBQyxRQUFRLENBQVUsR0FBVztRQUNqQyxNQUFNLENBQUMsR0FBRyxDQUFDLEdBQUcsTUFBTSxJQUFJLENBQUMsZ0JBQWdCLENBQUMsRUFBRSxHQUFHLEVBQUUsRUFBRSxFQUFFLElBQUksRUFBRSxDQUFDLEVBQUUsQ0FBQyxDQUFBO1FBQy9ELE9BQVEsR0FBRyxFQUFFLEtBQVcsSUFBSSxJQUFJLENBQUE7SUFDbEMsQ0FBQztJQUVELG1DQUFtQztJQUNuQyxLQUFLLENBQUMsUUFBUSxDQUFVLEdBQVcsRUFBRSxLQUFRO1FBQzNDLE1BQU0sQ0FBQyxHQUFHLEtBQTJDLENBQUE7UUFDckQsTUFBTSxDQUFDLFFBQVEsQ0FBQyxHQUFHLE1BQU0sSUFBSSxDQUFDLGdCQUFnQixDQUFDLEVBQUUsR0FBRyxFQUFFLEVBQUUsRUFBRSxJQUFJLEVBQUUsQ0FBQyxFQUFFLENBQUMsQ0FBQTtRQUNwRSxJQUFJLFFBQVEsRUFBRSxDQUFDO1lBQ2IsTUFBTSxJQUFJLENBQUMsa0JBQWtCLENBQUMsRUFBRSxFQUFFLEVBQUUsUUFBUSxDQUFDLEVBQUUsRUFBRSxLQUFLLEVBQUUsQ0FBQyxFQUFFLENBQUMsQ0FBQTtRQUM5RCxDQUFDO2FBQU0sQ0FBQztZQUNOLE1BQU0sSUFBSSxDQUFDLGtCQUFrQixDQUFDLEVBQUUsR0FBRyxFQUFFLEtBQUssRUFBRSxDQUFDLEVBQUUsQ0FBQyxDQUFBO1FBQ2xELENBQUM7UUFDRCxPQUFPLEtBQUssQ0FBQTtJQUNkLENBQUM7Q0FDRjtBQUVELGtCQUFlLGtCQUFrQixDQUFBIn0=
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.config = void 0;
|
|
4
|
+
exports.default = orderPlacedHandler;
|
|
5
|
+
const queue_1 = require("../lib/queue");
|
|
6
|
+
/**
|
|
7
|
+
* On every placed order, enqueue an Odoo sync job. The worker itself checks the
|
|
8
|
+
* `orderSync` master toggle and idempotency, so it's safe to always enqueue.
|
|
9
|
+
*/
|
|
10
|
+
async function orderPlacedHandler({ event, container, }) {
|
|
11
|
+
const orderId = event.data.id;
|
|
12
|
+
try {
|
|
13
|
+
await (0, queue_1.getOrderSyncQueue)().add("sync", { orderId }, { removeOnComplete: 100, removeOnFail: 100 });
|
|
14
|
+
}
|
|
15
|
+
catch (e) {
|
|
16
|
+
container
|
|
17
|
+
.resolve("logger")
|
|
18
|
+
?.error?.(`[odoo] could not queue order ${orderId}: ${e?.message}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
exports.config = {
|
|
22
|
+
event: "order.placed",
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3JkZXItcGxhY2VkLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vc3JjL3N1YnNjcmliZXJzL29yZGVyLXBsYWNlZC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFPQSxxQ0FnQkM7QUF0QkQsd0NBQWdEO0FBRWhEOzs7R0FHRztBQUNZLEtBQUssVUFBVSxrQkFBa0IsQ0FBQyxFQUMvQyxLQUFLLEVBQ0wsU0FBUyxHQUNzQjtJQUMvQixNQUFNLE9BQU8sR0FBRyxLQUFLLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQTtJQUM3QixJQUFJLENBQUM7UUFDSCxNQUFNLElBQUEseUJBQWlCLEdBQUUsQ0FBQyxHQUFHLENBQzNCLE1BQU0sRUFDTixFQUFFLE9BQU8sRUFBRSxFQUNYLEVBQUUsZ0JBQWdCLEVBQUUsR0FBRyxFQUFFLFlBQVksRUFBRSxHQUFHLEVBQUUsQ0FDN0MsQ0FBQTtJQUNILENBQUM7SUFBQyxPQUFPLENBQU0sRUFBRSxDQUFDO1FBQ2hCLFNBQVM7YUFDTixPQUFPLENBQUMsUUFBUSxDQUFDO1lBQ2xCLEVBQUUsS0FBSyxFQUFFLENBQUMsZ0NBQWdDLE9BQU8sS0FBSyxDQUFDLEVBQUUsT0FBTyxFQUFFLENBQUMsQ0FBQTtJQUN2RSxDQUFDO0FBQ0gsQ0FBQztBQUVZLFFBQUEsTUFBTSxHQUFxQjtJQUN0QyxLQUFLLEVBQUUsY0FBYztDQUN0QixDQUFBIn0=
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi9zcmMvd29ya2Zsb3dzL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiIifQ==
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 <YOUR NAME OR ORGANISATION>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# medusa-odoo-connector
|
|
2
|
+
|
|
3
|
+
A **Medusa v2** plugin that syncs your store with **Odoo** — products, inventory, orders, and customers — with a built-in admin UI and tiered (Stripe-billed) plans.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/medusa-odoo-connector)
|
|
6
|
+
|
|
7
|
+
> **Architecture:** this plugin runs inside Medusa and talks to a small companion
|
|
8
|
+
> service (**odoo-api**) over HTTP, which does the actual Odoo JSON-RPC. The
|
|
9
|
+
> plugin never holds Odoo credentials in the browser. You deploy `odoo-api`
|
|
10
|
+
> alongside Medusa (it ships in this repo).
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
| Area | What it does |
|
|
17
|
+
|------|--------------|
|
|
18
|
+
| **Products** | Import (Odoo → Medusa) and export (Medusa → Odoo) with variants, options, **per-variant prices**, images, and title-slug handles. Idempotent; variant reconciliation on update (create new / update existing, never delete). |
|
|
19
|
+
| **Inventory** | Two-way stock sync by SKU, mapped per location ↔ Odoo warehouse (on-hand / forecasted). |
|
|
20
|
+
| **Orders** | Push Medusa orders to Odoo as sale orders (+ optional invoice & payment). Auto on placement or manual. Atomic fail on unmatched SKU. |
|
|
21
|
+
| **Customers** | Two-way customer sync (matched by email). |
|
|
22
|
+
| **Scheduling** | Background imports via BullMQ + Redis (off / 30 min / hourly / twice-daily / daily). |
|
|
23
|
+
| **Admin UI** | Setup wizard, Dashboard, and a settings page per entity, plus Sync, Logs, and Plans. |
|
|
24
|
+
| **Plans & billing** | Free / Starter / Pro / Enterprise tiers that **gate** what syncs (product caps, sync direction, schedule frequency, entity access), with optional **Stripe** subscriptions. |
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Requirements
|
|
29
|
+
|
|
30
|
+
- Medusa **v2** (tested on `2.15.x`)
|
|
31
|
+
- Node **>= 20**
|
|
32
|
+
- **Redis** (for scheduled/queued syncs)
|
|
33
|
+
- A running **odoo-api** companion service (see below)
|
|
34
|
+
- An Odoo instance with API access (URL, database, username, API key)
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install medusa-odoo-connector
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Register the plugin and its module in `medusa-config.ts`:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
module.exports = defineConfig({
|
|
48
|
+
// ...
|
|
49
|
+
plugins: [
|
|
50
|
+
{ resolve: "medusa-odoo-connector", options: {} },
|
|
51
|
+
],
|
|
52
|
+
modules: [
|
|
53
|
+
{ resolve: "medusa-odoo-connector/modules/odoo" },
|
|
54
|
+
],
|
|
55
|
+
})
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Point the plugin at your odoo-api service via env:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
ODOO_API_URL=http://localhost:4000 # where odoo-api is reachable
|
|
62
|
+
ODOO_API_KEY=change-me # shared secret (matches odoo-api's API_KEY)
|
|
63
|
+
REDIS_URL=redis://localhost:6379 # for the sync queue/scheduler
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Then build the admin and start Medusa:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npx medusa build && npx medusa start
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
You'll find **Odoo Connector** in the Medusa admin sidebar.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## The odoo-api companion service
|
|
77
|
+
|
|
78
|
+
The plugin proxies all real Odoo work to **odoo-api** (an Express + MySQL service).
|
|
79
|
+
Deploy it separately and set its MySQL, the shared `API_KEY`, and (optionally)
|
|
80
|
+
`STRIPE_*` in its env. It does the Odoo JSON-RPC and optional Stripe billing;
|
|
81
|
+
Medusa stays the source of truth for configuration.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Stripe billing (optional)
|
|
86
|
+
|
|
87
|
+
The Plans page can sell tiered subscriptions via Stripe. Configure
|
|
88
|
+
`STRIPE_SECRET_KEY`, price IDs, and a webhook on the odoo-api service; the plan a
|
|
89
|
+
customer is on then gates what the connector will sync. Billing is entirely
|
|
90
|
+
optional — without Stripe keys the connector still runs (everyone is treated as
|
|
91
|
+
Free).
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Development
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npm run build # medusa plugin:build
|
|
99
|
+
npm run test:plugin # Vitest unit tests for the sync logic (run from the repo root)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
[MIT](./LICENSE)
|
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@odooconnector/medusa-odoo-connector",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Medusa v2 ↔ Odoo connector: two-way sync of products, inventory, orders and customers, with an admin UI and tiered Stripe-billed plans.",
|
|
5
|
+
"author": "odooconnector",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"organization": "odooconnector",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"medusa",
|
|
10
|
+
"medusa-plugin",
|
|
11
|
+
"medusa-v2",
|
|
12
|
+
"odoo",
|
|
13
|
+
"odoo-connector",
|
|
14
|
+
"erp",
|
|
15
|
+
"ecommerce",
|
|
16
|
+
"sync",
|
|
17
|
+
"inventory",
|
|
18
|
+
"stripe"
|
|
19
|
+
],
|
|
20
|
+
"homepage": "https://github.com/manishgautamwork/medusa-odoo-connector#readme",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/manishgautamwork/medusa-odoo-connector.git"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/manishgautamwork/medusa-odoo-connector/issues"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
".medusa/server"
|
|
33
|
+
],
|
|
34
|
+
"exports": {
|
|
35
|
+
"./package.json": "./package.json",
|
|
36
|
+
"./.medusa/server/src/modules/*": "./.medusa/server/src/modules/*/index.js",
|
|
37
|
+
"./modules/*": "./.medusa/server/src/modules/*/index.js",
|
|
38
|
+
"./*": "./.medusa/server/src/*.js",
|
|
39
|
+
"./admin": {
|
|
40
|
+
"import": "./.medusa/server/src/admin/index.mjs",
|
|
41
|
+
"require": "./.medusa/server/src/admin/index.js",
|
|
42
|
+
"default": "./.medusa/server/src/admin/index.js"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "medusa plugin:build",
|
|
47
|
+
"dev": "medusa plugin:develop",
|
|
48
|
+
"prepublishOnly": "medusa plugin:build"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"bullmq": "^5.34.0",
|
|
52
|
+
"ioredis": "^5.4.1"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@medusajs/admin-sdk": "2.15.5",
|
|
56
|
+
"@medusajs/cli": "2.15.5",
|
|
57
|
+
"@medusajs/framework": "2.15.5",
|
|
58
|
+
"@medusajs/icons": "2.15.5",
|
|
59
|
+
"@medusajs/medusa": "2.15.5",
|
|
60
|
+
"@medusajs/ui": "4.1.15",
|
|
61
|
+
"@swc/core": "^1.7.28",
|
|
62
|
+
"@types/node": "^20.0.0",
|
|
63
|
+
"@types/react": "^18.3.2",
|
|
64
|
+
"@types/react-dom": "^18.2.25",
|
|
65
|
+
"react": "^18.2.0",
|
|
66
|
+
"react-dom": "^18.2.0",
|
|
67
|
+
"typescript": "^5.6.2",
|
|
68
|
+
"vite": "^5.2.11"
|
|
69
|
+
},
|
|
70
|
+
"peerDependencies": {
|
|
71
|
+
"@medusajs/admin-sdk": "2.15.5",
|
|
72
|
+
"@medusajs/framework": "2.15.5",
|
|
73
|
+
"@medusajs/icons": "2.15.5",
|
|
74
|
+
"@medusajs/ui": "4.1.15",
|
|
75
|
+
"react": "^18.2.0",
|
|
76
|
+
"react-dom": "^18.2.0"
|
|
77
|
+
},
|
|
78
|
+
"engines": {
|
|
79
|
+
"node": ">=20"
|
|
80
|
+
}
|
|
81
|
+
}
|