Open Source: RecycleCore Industrial ERP | Full 12-Module Source Library

I. Engineering Philosophy: The Data-Driven Ecosystem

In the transition to a global circular economy, the most significant barrier for small and medium recycling facilities is not physical infrastructure, but Information Asymmetry. Traditional enterprise resource planning (ERP) systems are often cost-prohibitive, while manual spreadsheets suffer from high human-error rates and 72-hour data latency. RecycleCore ERP provides a serverless middle-ground: enterprise logic with zero infrastructure overhead.

This suite handles the entire lifecycle of recycled material—from initial weigh-bridge intake to sorting, granulation, and final B2B sales. By open-sourcing this architecture, we aim to provide facilities with the tools required to optimize their margins and reduce industrial waste through technical precision.

Enterprise Deployment Support

Need a Production-Ready Deployment?

Manually configuring a 12-module ERP requires precision. If you prefer our engineering team to handle the secure hosting, database setup, and custom branding for your facility, initialize a node today.

II. Functional Hierarchy (12 Modules)

The system is architected as a modular ecosystem. Each node can function as a standalone utility or as part of the integrated industrial suite.

  • Modules 1 & 2: Intake Intelligence: Automated procurement logs for Metals and Plastics with real-time VAT (5%) and liability calculation.
  • Modules 3, 4, 5: Industrial Processing: Tracking raw material sorting, granule production, and third-party recycling services with integrated "Loss-Tons" auditing.
  • Module 6: Intelligent Inventory: A real-time aggregation engine that performs net-stock subtraction (Total Purchases - Total Sales).
  • Module 7: Revenue Operations: A centralized sales ledger with automated professional invoice generation and status tracking.
  • Modules 8, 9, 10: Fiscal Governance: Monitoring of CapEx (Investments), OpEx (Expenses), and human capital (Wages/Payroll).
  • Modules 11 & 12: Analytics & Growth: Customer Relationship Management (CRM) for buyer tracking and the Executive Financial Dashboard.

III. The Data Schema (Sheet Architecture)

To ensure high-performance execution, your Google Sheets database must match this schema. Headers must be placed in Row 1 of each corresponding tab.

Module Sheet Name Critical Headers (A, B, C...)
Metal/PlasticMetalPurchasesDate, Type, Vendor, Tons, Price/Ton, Total (AED), VAT (AED), Paid, Ready to Sell, Comments
ProductionGranuleProductionDate, Color, Raw Material, Input Tons, Output Tons, Cost/Ton, Total Cost, Ready to Sell
RevenueSalesDate, Product, Buyer, Email, Tons, Price/Ton, Total (AED), VAT (AED), Paid, Invoice No, Status
PayrollWagesDate, Worker, Role, Joining Date, Monthly Wage, Extra Hour, Extra Hour Rate, Total, Paid

IV. Backend Infrastructure (Code.gs)

The backend serves as the RESTful API for all 12 modules. It handles complex CRUD operations, financial calculations, and data lookups. Paste this code into your Google Apps Script editor.

/**
 * ERP/Recycling Platform Google Apps Script Backend (Code.gs)
 *
 * This script serves as the centralized backend for all 12 Blogger modules,
 * handling all CRUD operations, data lookups, calculations (VAT, Totals),
 * and advanced data aggregation for Inventory and Dashboard reports.
 */

const SPREADSHEET_NAME = "RecyclingERP_Data";
const VAT_RATE = 0.05; 
const DEFAULT_SHEET = "Home";

const MODULE_CONFIGS = {
  MetalPurchases: {
    sheet: "MetalPurchases",
    headers: ["Date", "Type", "Vendor", "Tons", "Price/Ton", "Total (AED)", "VAT (AED)", "Paid", "Ready to Sell", "Comments"],
    calculations: (data) => calculatePurchases(data, VAT_RATE)
  },
  PlasticPurchases: {
    sheet: "PlasticPurchases",
    headers: ["Date", "Type", "Vendor", "Tons", "Price/Ton", "Total (AED)", "VAT (AED)", "Paid", "Ready to Sell", "Comments"],
    calculations: (data) => calculatePurchases(data, VAT_RATE)
  },
  SortedMetals: {
    sheet: "SortedMetals",
    headers: ["Date", "Input Batch", "Output Type", "Sorted Tons", "Loss Tons", "Ready to Sell", "Remarks"],
    calculations: (data) => data
  },
  GranuleProduction: {
    sheet: "GranuleProduction",
    headers: ["Date", "Color", "Raw Material Type", "Input Tons", "Output Tons", "Cost/Ton (AED)", "Total Cost (AED)", "Ready to Sell", "Comments"],
    calculations: (data) => calculateGranuleProduction(data)
  },
  RecyclingServices: {
    sheet: "RecyclingServices",
    headers: ["Date", "Client Name", "Plastic Type (Input)", "Input Tons", "Output Granule Type", "Output Tons", "Service Fee/Ton", "Total Service Fee (AED)", "Paid", "Ready to Sell", "Comments"],
    calculations: (data) => calculateRecyclingServices(data)
  },
  Sales: {
    sheet: "Sales",
    headers: ["Date", "Product", "Buyer", "Buyer Email", "Tons", "Price/Ton", "Total (AED)", "VAT (AED)", "Paid", "Invoice No.", "Status"],
    calculations: (data) => calculateSales(data, VAT_RATE)
  },
  Investments: {
    sheet: "Investments",
    headers: ["Date", "Investor", "Amount (AED)", "Purpose", "Mode", "Comments"],
    calculations: (data) => data
  },
  Expenses: {
    sheet: "Expenses",
    headers: ["Date", "Category", "Description", "Amount", "Is Vatable?", "VAT (AED)", "Paid By", "Ref"],
    calculations: (data) => calculateExpenses(data, VAT_RATE)
  },
  Wages: {
    sheet: "Wages",
    headers: ["Date", "Worker", "Role", "Date of Joining", "Monthly Wages", "Extra Hour", "Extra Hour Rate", "Total (AED)", "Paid", "Comments"],
    calculations: (data) => calculateWages(data)
  },
  CRMCustomers: {
    sheet: "CRM_Customers",
    headers: ["Name", "Email", "Phone", "Product Interest", "Notify on New Stock?"],
    calculations: (data) => data
  }
};

function getSpreadsheet() {
  let ss = SpreadsheetApp.getActiveSpreadsheet();
  if (!ss) ss = SpreadsheetApp.openByName(SPREADSHEET_NAME) || SpreadsheetApp.create(SPREADSHEET_NAME);
  for (let module in MODULE_CONFIGS) {
    const sheetName = MODULE_CONFIGS[module].sheet;
    let sheet = ss.getSheetByName(sheetName);
    if (!sheet) {
      sheet = ss.insertSheet(sheetName);
      sheet.appendRow(MODULE_CONFIGS[module].headers);
    }
  }
  return ss;
}

function calculatePurchases(data, vatRate) {
  const tons = parseFloat(data.Tons || 0);
  const price = parseFloat(data["Price/Ton"] || 0);
  if (isNaN(tons) || isNaN(price)) return data;
  const totalBeforeVat = tons * price;
  const vat = totalBeforeVat * vatRate;
  data["Total (AED)"] = (totalBeforeVat + vat).toFixed(2);
  data["VAT (AED)"] = vat.toFixed(2);
  return data;
}

function calculateGranuleProduction(data) {
  const outputTons = parseFloat(data.InputTons || 0);
  const cost = parseFloat(data["Cost/Ton"] || 0);
  if (!isNaN(outputTons) && !isNaN(cost)) data["Total Cost (AED)"] = (outputTons * cost).toFixed(2);
  return data;
}

function calculateRecyclingServices(data) {
  const outputTons = parseFloat(data.OutputTons || 0);
  const serviceFee = parseFloat(data["Service Fee/Ton"] || 0);
  if (!isNaN(outputTons) && !isNaN(serviceFee)) data["Total Service Fee (AED)"] = (outputTons * serviceFee).toFixed(2);
  return data;
}

function calculateSales(data, vatRate) {
  const tons = parseFloat(data.Tons || 0);
  const price = parseFloat(data["Price/Ton"] || 0);
  if (isNaN(tons) || isNaN(price)) return data;
  const totalBeforeVat = tons * price;
  const vat = totalBeforeVat * vatRate;
  data["Total (AED)"] = (totalBeforeVat + vat).toFixed(2);
  data["VAT (AED)"] = vat.toFixed(2);
  if (!data["Invoice No."]) data["Invoice No."] = "INV-" + Date.now().toString().slice(-6);
  return data;
}

function calculateExpenses(data, vatRate) {
  const amount = parseFloat(data.Amount || 0);
  let vat = (data["Is Vatable?"] === "Yes" && !isNaN(amount)) ? amount * vatRate : 0;
  data["VAT (AED)"] = vat.toFixed(2);
  return data;
}

function calculateWages(data) {
  const monthlyWages = parseFloat(data["Monthly Wages"] || 0);
  const extraHours = parseFloat(data["Extra Hour"] || 0);
  const rate = parseFloat(data["Extra Hour Rate"] || 0);
  if (isNaN(monthlyWages) || isNaN(extraHours) || isNaN(rate)) return data;
  data["Total (AED)"] = (monthlyWages + (extraHours * rate)).toFixed(2);
  return data;
}

function getData(sheetName) {
  const ss = getSpreadsheet();
  const sheet = ss.getSheetByName(sheetName);
  if (!sheet) return [];
  const values = sheet.getDataRange().getValues();
  if (values.length <= 1) return [];
  const headers = values[0];
  return values.slice(1).map((row, index) => {
    const rowObj = { rowId: index + 2 };
    headers.forEach((header, i) => {
      let value = row[i];
      if (value instanceof Date) value = Utilities.formatDate(value, ss.getSpreadsheetTimeZone(), "yyyy-MM-dd");
      else if (typeof value === 'number') value = value.toString();
      rowObj[header] = value;
    });
    return rowObj;
  });
}

function addRow(sheetName, data) {
  const ss = getSpreadsheet();
  const sheet = ss.getSheetByName(sheetName);
  const moduleName = sheetName.replace(/ /g, '');
  const headers = MODULE_CONFIGS[moduleName]?.headers;
  if (!sheet || !headers) throw new Error("Config not found.");
  const rowData = headers.map(header => {
    let value = data[header] || "";
    if (!isNaN(parseFloat(value)) && isFinite(value)) value = parseFloat(value);
    return value;
  });
  sheet.appendRow(rowData);
  return { success: true };
}

function updateRow(sheetName, data) {
  const ss = getSpreadsheet();
  const sheet = ss.getSheetByName(sheetName);
  const rowId = parseInt(data.rowId);
  const moduleName = sheetName.replace(/ /g, '');
  const headers = MODULE_CONFIGS[moduleName]?.headers;
  if (!sheet || isNaN(rowId)) throw new Error("Invalid ID.");
  const rowData = headers.map(header => {
    let value = data[header] || "";
    if (!isNaN(parseFloat(value)) && isFinite(value)) value = parseFloat(value);
    return value;
  });
  sheet.getRange(rowId, 1, 1, rowData.length).setValues([rowData]);
  return { success: true };
}

function deleteRow(sheetName, rowId) {
  const ss = getSpreadsheet();
  const sheet = ss.getSheetByName(sheetName);
  if (!sheet || isNaN(rowId)) throw new Error("Invalid ID.");
  sheet.deleteRow(parseInt(rowId));
  return { success: true };
}

function aggregateInventory() {
  const inventory = {};
  const inSheets = ["MetalPurchases", "PlasticPurchases", "SortedMetals", "GranuleProduction", "RecyclingServices"];
  inSheets.forEach(sheetName => {
    const data = getData(sheetName);
    data.forEach(row => {
      if (row["Ready to Sell"] === "Yes" || row["Ready to Sell"] === true) {
        let product = row.Type || row["Output Type"] || row["Output Granule Type"] || 'Unknown';
        let tons = parseFloat(row.Tons || row["Sorted Tons"] || row["Output Tons"] || 0);
        if (tons > 0) {
          if (!inventory[product]) inventory[product] = { tons: 0 };
          inventory[product].tons += tons;
        }
      }
    });
  });
  const salesData = getData("Sales");
  salesData.forEach(sale => {
    const product = sale.Product;
    const tonsSold = parseFloat(sale.Tons || 0);
    if (tonsSold > 0 && product && inventory[product]) inventory[product].tons -= tonsSold;
  });
  return Object.keys(inventory).map(p => ({ product: p, tons: inventory[p].tons.toFixed(2) }));
}

function doGet(e) {
  const action = e.parameter.action;
  const module = e.parameter.module;
  let response;
  if (action === 'getInventory') response = aggregateInventory();
  else if (module && MODULE_CONFIGS[module]) response = getData(MODULE_CONFIGS[module].sheet);
  return ContentService.createTextOutput(JSON.stringify(response)).setMimeType(ContentService.MimeType.JSON);
}

function doPost(e) {
  const params = e.parameter;
  const config = MODULE_CONFIGS[params.module];
  let data = config.calculations ? config.calculations({...params}) : {...params};
  let res;
  if (params.action === 'add') res = addRow(config.sheet, data);
  else if (params.action === 'update') res = updateRow(config.sheet, data);
  else if (params.action === 'delete') res = deleteRow(config.sheet, params.rowId);
  return ContentService.createTextOutput(JSON.stringify(res)).setMimeType(ContentService.MimeType.JSON);
}

function initializeSheets() { getSpreadsheet(); }
                

V. Frontend Module Library (12 Pages)

Select a module below to view and copy its specific Frontend HTML. These modules are optimized for deployment as standalone Blogger pages.

<!-- [PASTE FRONTEND PAGE 01: METAL PURCHASES HERE] -->
<!-- [PASTE FRONTEND PAGE 02: PLASTIC PURCHASES HERE] -->
<!-- [PASTE FRONTEND PAGE 03: SORTED METALS HERE] -->
<!-- [PASTE FRONTEND PAGE 04: GRANULE PRODUCTION HERE] -->
<!-- [PASTE FRONTEND PAGE 05: RECYCLING SERVICES HERE] -->
<!-- [PASTE FRONTEND PAGE 06: INVENTORY STOCK HERE] -->
<!-- [PASTE FRONTEND PAGE 07: SALES LEDGER HERE] -->
<!-- [PASTE FRONTEND PAGE 08: INVESTMENTS HERE] -->
<!-- [PASTE FRONTEND PAGE 09: EXPENSES HERE] -->
<!-- [PASTE FRONTEND PAGE 10: WAGES HERE] -->
<!-- [PASTE FRONTEND PAGE 11: CRM CUSTOMERS HERE] -->
<!-- [PASTE FRONTEND PAGE 12: EXECUTIVE DASHBOARD HERE] -->

VI. Security & Governance

Unlike standard SaaS models that store your proprietary business data on centralized third-party servers, RecycleCore utilizes a Zero-Knowledge Data Architecture. Your material grades, price sheets, and vendor ledgers stay entirely within your encrypted Google Workspace environment.

The Blogger frontend acts strictly as a "Logical Lens," transmitting data through secure HTTPS tunnels directly to your private Google Apps Script endpoint. Even if the public-facing blog is compromised, your operational records remain inaccessible to unauthorized parties. This follows the Local-First Privacy Model, which is the gold standard for modern industrial digital security.

VII. Deployment FAQ for Developers

How do I handle CORS errors?

Blogger and Google Apps Script are both within the Google ecosystem, but cross-origin requests are handled via the no-cors mode or the ContentService text output. Ensure your GAS deployment is set to "Anyone" access.

Can I scale past 50,000 rows?

Google Sheets has a 10-million cell limit. For a typical recycling facility, this ERP can store up to 5 years of high-volume transactions before requiring a data archive to a secondary sheet.

© 2026 ProScriptStack Engineering | Ref: FULL-ERP-SUITE-12 | Open Source Distribution