Compare commits
No commits in common. "master" and "kzrl/64-editable-payment-terms" have entirely different histories.
master
...
kzrl/64-ed
10
.gitignore
vendored
10
.gitignore
vendored
|
|
@ -2,8 +2,6 @@ app/tmp/*
|
|||
*.tar.gz
|
||||
*.swp
|
||||
*.swo
|
||||
.env.prod
|
||||
.env.stg
|
||||
app/vendors/tcpdf/cache/*
|
||||
app/tests/*
|
||||
app/emails/*
|
||||
|
|
@ -13,11 +11,3 @@ app/vaultmsgs/*
|
|||
app/cake_eclipse_helper.php
|
||||
app/webroot/pdf/*
|
||||
app/webroot/attachments_files/*
|
||||
backups/*
|
||||
|
||||
# Go binaries
|
||||
go/server
|
||||
go/vault
|
||||
go/go.mod
|
||||
go/go.sum
|
||||
go/goose.env
|
||||
|
|
|
|||
122
CLAUDE.md
122
CLAUDE.md
|
|
@ -1,122 +0,0 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Application Overview
|
||||
|
||||
CMC Sales is a B2B sales management system for CMC Technologies. The codebase consists of:
|
||||
|
||||
- **Legacy CakePHP 1.2.5 application** (in `/php/`) - Primary business logic
|
||||
- **Modern Go API** (in `/go/`) - New development using sqlc and Gorilla Mux
|
||||
|
||||
**Note**: Documentation also references a Django application that is not present in the current codebase.
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Docker Development
|
||||
```bash
|
||||
# Start all services (both CakePHP and Go applications)
|
||||
docker compose up
|
||||
|
||||
# Start specific services
|
||||
docker compose up cmc-php # CakePHP app only
|
||||
docker compose up cmc-go # Go app only
|
||||
docker compose up db # Database only
|
||||
|
||||
# Restore database from backup (get backups via rsync first)
|
||||
rsync -avz --progress cmc@sales.cmctechnologies.com.au:~/backups .
|
||||
gunzip < backups/backup_*.sql.gz | mariadb -h 127.0.0.1 -u cmc -p cmc
|
||||
|
||||
# Access development sites (add to /etc/hosts: 127.0.0.1 cmclocal)
|
||||
# http://cmclocal (CakePHP legacy app via nginx)
|
||||
# http://localhost:8080 (Go modern app direct access)
|
||||
```
|
||||
|
||||
### Testing
|
||||
```bash
|
||||
# CakePHP has minimal test coverage
|
||||
# Test files located in /app/tests/
|
||||
# Uses SimpleTest framework
|
||||
```
|
||||
|
||||
### Go Application Development
|
||||
```bash
|
||||
# Navigate to Go app directory
|
||||
cd go
|
||||
|
||||
# Configure private module access (first time only)
|
||||
go env -w GOPRIVATE=code.springupsoftware.com
|
||||
|
||||
# Install dependencies and sqlc
|
||||
make install
|
||||
|
||||
# Generate sqlc code from SQL queries
|
||||
make sqlc
|
||||
|
||||
# Run the Go server locally
|
||||
make run
|
||||
|
||||
# Run tests
|
||||
make test
|
||||
|
||||
# Build binary
|
||||
make build
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### CakePHP Application (Legacy)
|
||||
- **Framework**: CakePHP 1.2.5 (MVC pattern)
|
||||
- **PHP Version**: PHP 5 (Ubuntu Lucid 10.04 container)
|
||||
- **Location**: `/app/`
|
||||
- **Key Directories**:
|
||||
- `app/models/` - ActiveRecord models
|
||||
- `app/controllers/` - Request handlers
|
||||
- `app/views/` - Templates (.ctp files)
|
||||
- `app/config/` - Configuration including database.php
|
||||
- `app/vendors/` - Third-party libraries (TCPDF for PDFs, PHPExcel)
|
||||
- `app/webroot/` - Public assets and file uploads
|
||||
|
||||
### Go Application (Modern)
|
||||
- **Framework**: Gorilla Mux (HTTP router)
|
||||
- **Database**: sqlc for type-safe SQL queries
|
||||
- **Location**: `/go/`
|
||||
- **Structure**:
|
||||
- `cmd/server/` - Main application entry point
|
||||
- `internal/cmc/handlers/` - HTTP request handlers
|
||||
- `internal/cmc/db/` - Generated sqlc code
|
||||
- `sql/queries/` - SQL query definitions
|
||||
- `sql/schema/` - Database schema
|
||||
- **API Base Path**: `/api/v1`
|
||||
|
||||
### Database
|
||||
- **Engine**: MariaDB
|
||||
- **Host**: `db` (Docker service) or `127.0.0.1` locally
|
||||
- **Name**: `cmc`
|
||||
- **User**: `cmc`
|
||||
- **Password**: `xVRQI&cA?7AU=hqJ!%au` (hardcoded in `app/config/database.php`)
|
||||
|
||||
## Key Business Entities
|
||||
|
||||
- **Customers** - Client companies with ABN validation
|
||||
- **Purchase Orders** - Orders with suppliers (revision tracking enabled)
|
||||
- **Quotes** - Customer price quotations
|
||||
- **Invoices** - Billing documents
|
||||
- **Products** - Catalog with categories and options
|
||||
- **Shipments** - Logistics and freight management
|
||||
- **Jobs** - Project tracking
|
||||
- **Documents** - PDF generation via TCPDF
|
||||
- **Emails** - Correspondence tracking
|
||||
|
||||
## File Storage
|
||||
|
||||
- **PDF files**: `/app/webroot/pdf/` (mounted volume)
|
||||
- **Attachments**: `/app/webroot/attachments_files/` (mounted volume)
|
||||
|
||||
## Critical Considerations
|
||||
|
||||
- **Security**: Running on extremely outdated Ubuntu Lucid (10.04) with PHP 5
|
||||
- **Database Changes**: Exercise caution - the database may be shared with other applications
|
||||
- **CakePHP Conventions**: Uses 2008-era patterns and practices
|
||||
- **Authentication**: Basic auth configured via `userpasswd` file
|
||||
- **PDF Generation**: Uses TCPDF library in `/app/vendors/tcpdf/`
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
FROM ghcr.io/kzrl/ubuntu:lucid
|
||||
FROM ubuntu:lucid
|
||||
|
||||
# Set environment variables.
|
||||
ENV HOME /root
|
||||
|
|
@ -57,9 +57,5 @@ RUN chmod +x /var/www/cmc-sales/run_vault.sh
|
|||
RUN chmod +x /var/www/cmc-sales/run_update_invoices.sh
|
||||
|
||||
|
||||
# Ensure Apache error/access logs go to Docker stdout/stderr
|
||||
RUN ln -sf /dev/stdout /var/log/apache2/access.log && \
|
||||
ln -sf /dev/stderr /var/log/apache2/error.log
|
||||
|
||||
# By default, simply start apache.
|
||||
CMD /usr/sbin/apache2ctl -D FOREGROUND
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
FROM golang:1.24-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy go.mod and go.sum first
|
||||
COPY go/go.mod go/go.sum ./
|
||||
|
||||
# Download dependencies
|
||||
RUN go mod download
|
||||
|
||||
# Install Air for hot reload (pinned to v1.52.3 for Go 1.24 compatibility)
|
||||
RUN go install github.com/air-verse/air@v1.52.3
|
||||
# Install sqlc for SQL code generation
|
||||
RUN go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
|
||||
|
||||
# Copy source code
|
||||
COPY go/ .
|
||||
|
||||
# Generate sqlc code
|
||||
RUN sqlc generate
|
||||
|
||||
# Copy Air config
|
||||
COPY go/.air.toml .air.toml
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD ["air", "-c", ".air.toml"]
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
# Use the official PHP 5.6 Apache image for classic mod_php
|
||||
FROM php:5.6-apache
|
||||
|
||||
# Install required system libraries and PHP extensions for CakePHP
|
||||
RUN sed -i 's|http://deb.debian.org/debian|http://archive.debian.org/debian|g' /etc/apt/sources.list && \
|
||||
sed -i 's|http://security.debian.org/debian-security|http://archive.debian.org/debian-security|g' /etc/apt/sources.list && \
|
||||
sed -i '/stretch-updates/d' /etc/apt/sources.list && \
|
||||
echo 'Acquire::AllowInsecureRepositories "true";' > /etc/apt/apt.conf.d/99allow-insecure && \
|
||||
echo 'Acquire::AllowDowngradeToInsecureRepositories "true";' >> /etc/apt/apt.conf.d/99allow-insecure && \
|
||||
apt-get update && \
|
||||
apt-get install --allow-unauthenticated -y libc-client2007e-dev libkrb5-dev libpng-dev libjpeg-dev libfreetype6-dev libcurl4-openssl-dev libxml2-dev libssl-dev libmcrypt-dev libicu-dev && \
|
||||
docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ && \
|
||||
docker-php-ext-configure imap --with-kerberos --with-imap-ssl && \
|
||||
docker-php-ext-install mysqli pdo pdo_mysql mbstring gd curl imap
|
||||
|
||||
# Set environment variables.
|
||||
ENV HOME /root
|
||||
|
||||
# Define working directory.
|
||||
WORKDIR /root
|
||||
|
||||
ARG COMMIT
|
||||
ENV COMMIT_SHA=${COMMIT}
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
# Copy vhost config to Apache's sites-available
|
||||
ADD conf/apache-vhost.conf /etc/apache2/sites-available/cmc-sales.conf
|
||||
ADD conf/ripmime /bin/ripmime
|
||||
|
||||
RUN chmod +x /bin/ripmime \
|
||||
&& a2ensite cmc-sales \
|
||||
&& a2dissite 000-default \
|
||||
&& a2enmod rewrite \
|
||||
&& a2enmod headers
|
||||
|
||||
# Copy site into place.
|
||||
ADD php/ /var/www/cmc-sales
|
||||
ADD php/app/config/database_local.php /var/www/cmc-sales/app/config/database.php
|
||||
RUN mkdir -p /var/www/cmc-sales/app/tmp
|
||||
RUN mkdir -p /var/www/cmc-sales/app/tmp/logs
|
||||
RUN chmod -R 755 /var/www/cmc-sales/app/tmp
|
||||
|
||||
# Ensure CakePHP tmp directory is writable by web server
|
||||
RUN chmod -R 777 /var/www/cmc-sales/app/tmp
|
||||
# By default, simply start apache.
|
||||
CMD /usr/sbin/apache2ctl -D FOREGROUND
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
FROM mariadb:latest
|
||||
|
||||
# Copy custom MariaDB configuration to disable strict mode
|
||||
COPY conf/mariadb-no-strict.cnf /etc/mysql/conf.d/
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
FROM golang:1.24-alpine AS builder
|
||||
|
||||
RUN apk add --no-cache git
|
||||
WORKDIR /app
|
||||
COPY go/go.mod go/go.sum ./
|
||||
RUN go mod download
|
||||
COPY go/ .
|
||||
RUN go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
|
||||
RUN sqlc generate
|
||||
RUN go mod tidy
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o server cmd/server/main.go
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o vault cmd/vault/main.go
|
||||
|
||||
FROM alpine:latest
|
||||
RUN apk --no-cache add ca-certificates tzdata
|
||||
WORKDIR /root/
|
||||
COPY --from=builder /app/server .
|
||||
COPY --from=builder /app/vault .
|
||||
COPY go/templates ./templates
|
||||
COPY go/static ./static
|
||||
COPY go/.env.example .env
|
||||
EXPOSE 8082
|
||||
CMD ["./server"]
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
# Use the official PHP 5.6 Apache image for classic mod_php
|
||||
FROM php:5.6-apache
|
||||
|
||||
# Install required system libraries and PHP extensions for CakePHP
|
||||
RUN sed -i 's|http://deb.debian.org/debian|http://archive.debian.org/debian|g' /etc/apt/sources.list && \
|
||||
sed -i 's|http://security.debian.org/debian-security|http://archive.debian.org/debian-security|g' /etc/apt/sources.list && \
|
||||
sed -i '/stretch-updates/d' /etc/apt/sources.list && \
|
||||
echo 'Acquire::AllowInsecureRepositories "true";' > /etc/apt/apt.conf.d/99allow-insecure && \
|
||||
echo 'Acquire::AllowDowngradeToInsecureRepositories "true";' >> /etc/apt/apt.conf.d/99allow-insecure && \
|
||||
apt-get update && \
|
||||
apt-get install --allow-unauthenticated -y libc-client2007e-dev libkrb5-dev libpng-dev libjpeg-dev libfreetype6-dev libcurl4-openssl-dev libxml2-dev libssl-dev libmcrypt-dev libicu-dev && \
|
||||
docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ && \
|
||||
docker-php-ext-configure imap --with-kerberos --with-imap-ssl && \
|
||||
docker-php-ext-install mysqli pdo pdo_mysql mbstring gd curl imap
|
||||
|
||||
# Set environment variables.
|
||||
ENV HOME /root
|
||||
|
||||
# Define working directory.
|
||||
WORKDIR /root
|
||||
|
||||
ARG COMMIT
|
||||
ENV COMMIT_SHA=${COMMIT}
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
# Copy vhost config to Apache's sites-available
|
||||
ADD conf/apache-vhost.conf /etc/apache2/sites-available/cmc-sales.conf
|
||||
ADD conf/ripmime /bin/ripmime
|
||||
|
||||
RUN chmod +x /bin/ripmime \
|
||||
&& a2ensite cmc-sales \
|
||||
&& a2dissite 000-default \
|
||||
&& a2enmod rewrite \
|
||||
&& a2enmod headers
|
||||
|
||||
# Copy site into place.
|
||||
ADD php/ /var/www/cmc-sales
|
||||
ADD php/app/config/database.php /var/www/cmc-sales/app/config/database.php
|
||||
RUN mkdir -p /var/www/cmc-sales/app/tmp
|
||||
RUN mkdir -p /var/www/cmc-sales/app/tmp/logs
|
||||
RUN chmod -R 755 /var/www/cmc-sales/app/tmp
|
||||
|
||||
# Ensure CakePHP tmp directory is writable by web server
|
||||
RUN chmod -R 777 /var/www/cmc-sales/app/tmp
|
||||
# By default, simply start apache.
|
||||
CMD /usr/sbin/apache2ctl -D FOREGROUND
|
||||
|
|
@ -1 +0,0 @@
|
|||
FROM mariadb:latest
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
FROM golang:1.24-alpine AS builder
|
||||
|
||||
RUN apk add --no-cache git
|
||||
WORKDIR /app
|
||||
COPY go/go.mod go/go.sum ./
|
||||
RUN go mod download
|
||||
COPY go/ .
|
||||
RUN go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
|
||||
RUN sqlc generate
|
||||
RUN go mod tidy
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o server cmd/server/main.go
|
||||
|
||||
FROM alpine:latest
|
||||
RUN apk --no-cache add ca-certificates tzdata
|
||||
WORKDIR /root/
|
||||
COPY --from=builder /app/server .
|
||||
COPY go/templates ./templates
|
||||
COPY go/static ./static
|
||||
COPY go/.env.example .env
|
||||
EXPOSE 8082
|
||||
CMD ["./server"]
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
|
||||
|
||||
# Use the official PHP 5.6 Apache image for classic mod_php
|
||||
FROM php:5.6-apache
|
||||
|
||||
# Install required system libraries and PHP extensions for CakePHP
|
||||
RUN sed -i 's|http://deb.debian.org/debian|http://archive.debian.org/debian|g' /etc/apt/sources.list && \
|
||||
sed -i 's|http://security.debian.org/debian-security|http://archive.debian.org/debian-security|g' /etc/apt/sources.list && \
|
||||
sed -i '/stretch-updates/d' /etc/apt/sources.list && \
|
||||
echo 'Acquire::AllowInsecureRepositories "true";' > /etc/apt/apt.conf.d/99allow-insecure && \
|
||||
echo 'Acquire::AllowDowngradeToInsecureRepositories "true";' >> /etc/apt/apt.conf.d/99allow-insecure && \
|
||||
apt-get update && \
|
||||
apt-get install --allow-unauthenticated -y libc-client2007e-dev libkrb5-dev libpng-dev libjpeg-dev libfreetype6-dev libcurl4-openssl-dev libxml2-dev libssl-dev libmcrypt-dev libicu-dev && \
|
||||
docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ && \
|
||||
docker-php-ext-configure imap --with-kerberos --with-imap-ssl && \
|
||||
docker-php-ext-install mysqli pdo pdo_mysql mbstring gd curl imap
|
||||
|
||||
# Set environment variables.
|
||||
ENV HOME /root
|
||||
|
||||
# Define working directory.
|
||||
WORKDIR /root
|
||||
|
||||
|
||||
|
||||
ARG COMMIT
|
||||
ENV COMMIT_SHA=${COMMIT}
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
# Legacy apt compatibility and install steps for Ubuntu 16.04 (now commented out)
|
||||
# RUN sed -i 's|http://archive.ubuntu.com/ubuntu/|http://old-releases.ubuntu.com/ubuntu/|g' /etc/apt/sources.list && \
|
||||
# sed -i 's|http://security.ubuntu.com/ubuntu|http://old-releases.ubuntu.com/ubuntu|g' /etc/apt/sources.list
|
||||
# RUN apt-get update
|
||||
# RUN apt-get -y upgrade
|
||||
# RUN echo 'Acquire::AllowInsecureRepositories "true";' > /etc/apt/apt.conf.d/99allow-insecure
|
||||
# RUN apt-get update -o Acquire::AllowInsecureRepositories=true --allow-unauthenticated
|
||||
# RUN DEBIAN_FRONTEND=noninteractive apt-get -y install apache2 libapache2-mod-php5 php5-mysql php5-gd php-pear php-apc php5-curl php5-imap
|
||||
# RUN a2enmod php5
|
||||
# RUN php5enmod openssl
|
||||
# RUN sed -i "s/short_open_tag = Off/short_open_tag = On/" /etc/php5/apache2/php.ini
|
||||
# RUN sed -i "s/error_reporting = .*$/error_reporting = E_ERROR | E_WARNING | E_PARSE/" /etc/php5/apache2/php.ini
|
||||
# ADD conf/php.ini /etc/php5/apache2/php.ini
|
||||
|
||||
|
||||
# Copy vhost config to Apache's sites-available
|
||||
ADD conf/apache-vhost.conf /etc/apache2/sites-available/cmc-sales.conf
|
||||
ADD conf/ripmime /bin/ripmime
|
||||
|
||||
RUN chmod +x /bin/ripmime \
|
||||
&& a2ensite cmc-sales \
|
||||
&& a2dissite 000-default \
|
||||
&& a2enmod rewrite \
|
||||
&& a2enmod headers
|
||||
|
||||
# Copy site into place.
|
||||
ADD php/ /var/www/cmc-sales
|
||||
ADD php/app/config/database_stg.php /var/www/cmc-sales/app/config/database.php
|
||||
RUN mkdir -p /var/www/cmc-sales/app/tmp
|
||||
RUN mkdir -p /var/www/cmc-sales/app/tmp/logs
|
||||
RUN chmod -R 755 /var/www/cmc-sales/app/tmp
|
||||
|
||||
# Ensure CakePHP tmp directory is writable by web server
|
||||
RUN chmod -R 777 /var/www/cmc-sales/app/tmp
|
||||
# No need to disable proxy_fcgi or remove PHP-FPM conf files in this image
|
||||
# By default, simply start apache.
|
||||
CMD /usr/sbin/apache2ctl -D FOREGROUND
|
||||
63
Dockerfile_stg
Normal file
63
Dockerfile_stg
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
# This is 99% the same as the prod one. I should do something smarter here.
|
||||
|
||||
FROM ubuntu:lucid
|
||||
|
||||
# Set environment variables.
|
||||
ENV HOME /root
|
||||
|
||||
# Define working directory.
|
||||
WORKDIR /root
|
||||
|
||||
RUN sed -i 's/archive/old-releases/' /etc/apt/sources.list
|
||||
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get -y upgrade
|
||||
|
||||
# Install apache, PHP, and supplimentary programs. curl and lynx-cur are for debugging the container.
|
||||
RUN DEBIAN_FRONTEND=noninteractive apt-get -y install apache2 libapache2-mod-php5 php5-mysql php5-gd php-pear php-apc php5-curl php5-imap
|
||||
|
||||
# Enable apache mods.
|
||||
#RUN php5enmod openssl
|
||||
RUN a2enmod php5
|
||||
RUN a2enmod rewrite
|
||||
RUN a2enmod headers
|
||||
|
||||
|
||||
# Update the PHP.ini file, enable <? ?> tags and quieten logging.
|
||||
# RUN sed -i "s/short_open_tag = Off/short_open_tag = On/" /etc/php5/apache2/php.ini
|
||||
#RUN sed -i "s/error_reporting = .*$/error_reporting = E_ERROR | E_WARNING | E_PARSE/" /etc/php5/apache2/php.ini
|
||||
|
||||
ADD conf/php.ini /etc/php5/apache2/php.ini
|
||||
|
||||
# Manually set up the apache environment variables
|
||||
ENV APACHE_RUN_USER www-data
|
||||
ENV APACHE_RUN_GROUP www-data
|
||||
ENV APACHE_LOG_DIR /var/log/apache2
|
||||
ENV APACHE_LOCK_DIR /var/lock/apache2
|
||||
ENV APACHE_PID_FILE /var/run/apache2.pid
|
||||
|
||||
ARG COMMIT
|
||||
ENV COMMIT_SHA=${COMMIT}
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
# Update the default apache site with the config we created.
|
||||
ADD conf/apache-vhost.conf /etc/apache2/sites-available/cmc-sales
|
||||
ADD conf/ripmime /bin/ripmime
|
||||
|
||||
RUN chmod +x /bin/ripmime
|
||||
RUN a2dissite 000-default
|
||||
|
||||
# Copy site into place.
|
||||
ADD . /var/www/cmc-sales
|
||||
ADD app/config/database_stg.php /var/www/cmc-sales/app/config/database.php
|
||||
RUN mkdir /var/www/cmc-sales/app/tmp
|
||||
RUN mkdir /var/www/cmc-sales/app/tmp/logs
|
||||
RUN chmod -R 755 /var/www/cmc-sales/app/tmp
|
||||
RUN chmod +x /var/www/cmc-sales/run_vault.sh
|
||||
|
||||
RUN a2ensite cmc-sales
|
||||
|
||||
# By default, simply start apache.
|
||||
CMD /usr/sbin/apache2ctl -D FOREGROUND
|
||||
18
MIGRATION.md
Normal file
18
MIGRATION.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# migration instructions
|
||||
|
||||
mysql -u cmc -p cmc < ~/migration/latest.sql
|
||||
|
||||
MariaDB [(none)]> CREATE USER 'cmc'@'172.17.0.2' IDENTIFIED BY 'somepass';
|
||||
Query OK, 0 rows affected (0.00 sec)
|
||||
|
||||
MariaDB [(none)]> GRANT ALL PRIVILEGES ON cmc.* TO 'cmc'@'172.17.0.2';
|
||||
|
||||
|
||||
www-data@helios:~$ du -hcs vaultmsgs
|
||||
64G vaultmsgs
|
||||
64G total
|
||||
www-data@helios:~$ du -hcs emails
|
||||
192G emails
|
||||
192G total
|
||||
www-data@helios:~$
|
||||
|
||||
167
README.md
167
README.md
|
|
@ -1,111 +1,102 @@
|
|||
# cmc-sales
|
||||
|
||||
CMC Sales is a business management system with two applications:
|
||||
|
||||
- **PHP Application**: CakePHP 1.2.5 (currently the primary application)
|
||||
- **Go Application**: Go + HTMX (used for select features, growing)
|
||||
|
||||
**Future development should be done in the Go application wherever possible.**
|
||||
|
||||
## Architecture
|
||||
|
||||
Both applications:
|
||||
- Share the same MariaDB database
|
||||
- Run behind a shared Caddy reverse proxy with basic authentication
|
||||
- Support staging and production environments on the same server
|
||||
|
||||
The PHP application currently handles most functionality, while the Go application is used for select screens and new features as they're developed.
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Quick Start
|
||||
|
||||
``` shell
|
||||
git clone git@code.springupsoftware.com:cmc/cmc-sales.git
|
||||
cd cmc-sales
|
||||
|
||||
# Easy way - use the setup script
|
||||
./start-development.sh
|
||||
|
||||
# Manual way
|
||||
rsync -avz --progress cmc@sales.cmctechnologies.com.au:~/backups .
|
||||
docker compose up --build
|
||||
gunzip < backups/backup_*.sql.gz | mariadb -h 127.0.0.1 -u cmc -p cmc
|
||||
```
|
||||
|
||||
### Access Applications
|
||||
|
||||
**Add to /etc/hosts:**
|
||||
```
|
||||
127.0.0.1 cmclocal
|
||||
```
|
||||
|
||||
**Application URLs:**
|
||||
- **CakePHP (Legacy)**: http://cmclocal - Original CakePHP 1.2.5 application
|
||||
- **Go (Modern)**: http://localhost:8080 - New Go application with HTMX frontend
|
||||
- **Database**: localhost:3306 (user: `cmc`, password: see `app/config/database.php`)
|
||||
|
||||
### Architecture
|
||||
|
||||
- **cmc-php**: Legacy CakePHP application (nginx proxied)
|
||||
- **cmc-go**: Modern Go application (direct access on port 8080)
|
||||
- **db**: Shared MariaDB database
|
||||
- **nginx**: Reverse proxy for CakePHP app
|
||||
|
||||
Both applications share the same database, allowing for gradual migration.
|
||||
## Install
|
||||
|
||||
### Requirements
|
||||
|
||||
- **Go Application**: Requires Go 1.23+ (for latest sqlc)
|
||||
- Alternative: Use `Dockerfile.go.legacy` with Go 1.21 and sqlc v1.26.0
|
||||
Debian or Ubuntu OS. These instructions written for Debian 9.9
|
||||
|
||||
## Deployment
|
||||
Assumed pre-work:
|
||||
|
||||
### Prerequisites
|
||||
Create a new VM with hostname newserver.cmctechnologies.com.au
|
||||
Configure DNS appropriately. cmctechnologies.com.au zones is currently managed in Google Cloud DNS on Karl's account:
|
||||
https://console.cloud.google.com/net-services/dns/zones/cmctechnologies?project=cmc-technologies&authuser=1&folder&organizationId
|
||||
|
||||
The deployment scripts use SSH to connect to the server. Configure your SSH config (`~/.ssh/config`) with a host entry named `cmc` pointing to the correct server:
|
||||
Will need to migrate that to CMC's GSuite account at some point.
|
||||
|
||||
|
||||
1. Install ansible on your workstation
|
||||
```
|
||||
apt-get install ansible
|
||||
```
|
||||
2. Clone the playbooks
|
||||
```
|
||||
git clone git@gitlab.com:minimalist.software/cmc-playbooks.git
|
||||
```
|
||||
3. Execute the playbooks
|
||||
|
||||
The nginx config expects the site to be available at sales.cmctechnologies.com.au.
|
||||
|
||||
You'll need to add the hostname to config/nginx-site, if this isn't sales.cmctechnologies.com.au
|
||||
|
||||
```
|
||||
Host cmc
|
||||
HostName node0.prd.springupsoftware.com
|
||||
User cmc
|
||||
IdentityFile ~/.ssh/cmc
|
||||
cd cmc-playbooks
|
||||
# Add the hostname of your new server to the inventory.txt
|
||||
ansible-playbook -i inventory.txt setup.yml
|
||||
```
|
||||
4. SSH to the new server and configure gitlab-runner
|
||||
```
|
||||
ssh newserver.cmctechnologies.com.au
|
||||
sudo gitlab-runner register
|
||||
```
|
||||
5. SSH to the new server as cmc user
|
||||
```
|
||||
ssh cmc@newserver.cmctechnologies.com.au
|
||||
```
|
||||
|
||||
### Deployment Procedures
|
||||
|
||||
Deploy to staging or production using the scripts in the `deploy/` directory:
|
||||
|
||||
**Deploy to Staging:**
|
||||
```bash
|
||||
./scripts/deploy/deploy-stg.sh
|
||||
6. Add the SSH key to the cmc-sales repo on gitlab as a deploy key
|
||||
https://gitlab.com/minimalist.software/cmc-sales/-/settings/repository
|
||||
```
|
||||
cmc@cmc:~$ cat .ssh/id_rsa.pub
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFIdoWVp2pGDb46ubW6jkfIpREMa/veD6xZVAtnj3WG1sX7NEUlQYq3RKbZ5CThlw6GKMSYoIsIqk7p6zSoJHGlJSLxoJ0edKflciMUFMTQrdm4T1USXsK+gd0C4DUCyVkYFOs37sy+JtziymnBTm7iOeVI3aMxwfoCOs6mNiD0ettjJT6WtVyy0ZTb6yU4uz7CHj1IGsvwsoKJWPGwJrZ/MfByNl6aJ8R/8zDwbtP06owKD4b3ZPgakM3nYRRoKzHZ/SClz50SXMKC4/nmFY9wLuuMhCWK+9x4/4VPSnxXESOlENMfUoa1IY4osAnZCtaFrWDyenJ+spZrNfgcscD ansible-generated on cmc
|
||||
```
|
||||
|
||||
**Deploy to Production:**
|
||||
```bash
|
||||
./deploy/deploy-prod.sh
|
||||
6. Clone the cmc-sales repo
|
||||
```
|
||||
git clone git@gitlab.com:minimalist.software/cmc-sales.git
|
||||
```
|
||||
|
||||
**Rebuild without cache (useful after dependency changes):**
|
||||
```bash
|
||||
./scripts/deploy/deploy-prod.sh --no-cache
|
||||
./scripts/deploy/deploy-stg.sh --no-cache
|
||||
7. As root on new server configure mySQL user cmc
|
||||
Note: get password from app/config/database.php
|
||||
(or set a new one and change it)
|
||||
```
|
||||
# mysql -u root
|
||||
CREATE USER 'cmc'@'localhost' IDENTIFIED BY 'password';
|
||||
CREATE USER 'cmc'@'172.17.0.2' IDENTIFIED BY 'password';
|
||||
CREATE database cmc;
|
||||
GRANT ALL PRIVILEGES ON cmc.* TO 'cmc'@'localhost';
|
||||
GRANT ALL PRIVILEGES ON cmc.* TO 'cmc'@'172.17.0.2';
|
||||
```
|
||||
|
||||
### How Deployment Works
|
||||
8. Get the latest backup from Google Drive
|
||||
|
||||
1. The deploy script connects to the server via the `cmc` SSH host
|
||||
2. Clones or updates the appropriate git branch (`stg` or `prod`)
|
||||
3. Creates environment configuration for the Go application
|
||||
4. Builds and starts Docker containers using the appropriate compose file
|
||||
5. Applications are accessible through Caddy reverse proxy with basic auth
|
||||
In the shared google drive:
|
||||
eg. backups/database/backup_20191217_21001.sql.gz
|
||||
|
||||
### Deployment Environments
|
||||
Copy up to the new server:
|
||||
```
|
||||
rsync backup_*.gz root@newserver:~/
|
||||
|
||||
- **Staging**: Branch `stg` → https://stg.cmctechnologies.com.au
|
||||
- **Production**: Branch `prod` → https://sales.cmctechnologies.com.au or https://prod.cmctechnologies.com.au
|
||||
```
|
||||
|
||||
Both environments run on the same server and share:
|
||||
- A single Caddy reverse proxy (handles HTTPS and basic authentication for both environments)
|
||||
- Separate Docker containers for each environment's PHP and Go applications
|
||||
- Separate MariaDB database instances
|
||||
9. Restore backup to cmc database
|
||||
```
|
||||
zcat backup_* | mysql -u cmc -p
|
||||
```
|
||||
|
||||
10. Redeploy from Gitlab
|
||||
https://gitlab.com/minimalist.software/cmc-sales/pipelines/new
|
||||
|
||||
11. You should have a new installation of cmc-sales.
|
||||
|
||||
12. Seems new Linux kernels break the docker
|
||||
https://github.com/moby/moby/issues/28705
|
||||
|
||||
|
||||
13. Mysql needs special args not to break
|
||||
|
||||
```
|
||||
# /etc/mysql/my.cnf
|
||||
sql_mode=ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
|
||||
```
|
||||
|
|
|
|||
|
|
@ -43,6 +43,4 @@
|
|||
*
|
||||
*/
|
||||
//EOF
|
||||
|
||||
require_once(dirname(__FILE__) . '/php7_compat.php');
|
||||
?>
|
||||
|
|
@ -47,13 +47,19 @@ Configure::write('version', '1.0.1');
|
|||
|
||||
$host = $_SERVER['HTTP_HOST'];
|
||||
|
||||
/*Configure::write('smtp_settings', array(
|
||||
'port' => '587',
|
||||
'timeout' => '60',
|
||||
'host' => 'smtp-relay.gmail.com',
|
||||
'username' => 'sales',
|
||||
'password' => 'S%s\'mMZ})MGsg$k!5N|mPSQ>}'
|
||||
));
|
||||
*/
|
||||
|
||||
// SMTP settings
|
||||
Configure::write('smtp_settings', array(
|
||||
'port' => '25',
|
||||
'timeout' => '30',
|
||||
'host' => 'postfix'
|
||||
));
|
||||
'host' => '172.17.0.1'));
|
||||
|
||||
//Production/Staging Config
|
||||
|
||||
|
|
@ -74,8 +80,8 @@ Cache::config('default', array(
|
|||
));
|
||||
|
||||
Configure::write('email_directory', '/var/www/emails');
|
||||
Configure::write('pdf_directory', $basedir.'webroot/pdf/');
|
||||
Configure::write('attachments_directory', $basedir.'webroot/attachments_files/');
|
||||
Configure::write('pdf_directory', $basedir.'/webroot/pdf/');
|
||||
Configure::write('attachments_directory', $basedir.'/webroot/attachments_files/');
|
||||
|
||||
|
||||
/**
|
||||
|
|
@ -168,30 +174,6 @@ Configure::write('Security.salt', 'uiPxR3MzVXAID5zucbxLdxP4TX33buPoCWZr4JfroGoaE
|
|||
Configure::write('Acl.classname', 'DbAcl');
|
||||
Configure::write('Acl.database', 'default');
|
||||
|
||||
/**
|
||||
* Tailscale Authentication Configuration
|
||||
*
|
||||
* Enable Tailscale HTTP header authentication support
|
||||
* When enabled, the system will check for Tailscale authentication headers
|
||||
* before falling back to HTTP Basic Auth
|
||||
*/
|
||||
Configure::write('Tailscale.enabled', true);
|
||||
|
||||
/**
|
||||
* Auto-create users from Tailscale authentication
|
||||
* When enabled, users authenticated via Tailscale headers will be
|
||||
* automatically created if they don't exist in the database
|
||||
*/
|
||||
Configure::write('Tailscale.autoCreateUsers', false);
|
||||
|
||||
/**
|
||||
* Default access level for auto-created Tailscale users
|
||||
* Options: 'user', 'manager', 'admin'
|
||||
*/
|
||||
Configure::write('Tailscale.defaultAccessLevel', 'user');
|
||||
|
||||
/**
|
||||
* Set timezone to Australian Eastern Time
|
||||
* This handles both AEST and AEDT (daylight saving) automatically
|
||||
*/
|
||||
date_default_timezone_set('Australia/Sydney');
|
||||
|
|
@ -7,9 +7,9 @@
|
|||
class DATABASE_CONFIG {
|
||||
|
||||
var $default = array(
|
||||
'driver' => 'mysqli',
|
||||
'driver' => 'mysql',
|
||||
'persistent' => false,
|
||||
'host' => 'cmc-prod-db',
|
||||
'host' => '172.17.0.1',
|
||||
'login' => 'cmc',
|
||||
'password' => 'xVRQI&cA?7AU=hqJ!%au',
|
||||
'database' => 'cmc',
|
||||
14
app/config/database_stg.php
Normal file
14
app/config/database_stg.php
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
class DATABASE_CONFIG {
|
||||
|
||||
var $default = array(
|
||||
'driver' => 'mysql',
|
||||
'persistent' => false,
|
||||
'host' => '172.17.0.1',
|
||||
'login' => 'staging',
|
||||
'password' => 'stagingmoopwoopVerySecure',
|
||||
'database' => 'staging',
|
||||
'prefix' => '',
|
||||
);
|
||||
}
|
||||
143
app/controllers/app_controller.php
Executable file
143
app/controllers/app_controller.php
Executable file
|
|
@ -0,0 +1,143 @@
|
|||
<?php
|
||||
|
||||
/* App Controller */
|
||||
|
||||
class AppController extends Controller {
|
||||
|
||||
var $components = array('RequestHandler');
|
||||
|
||||
var $uses = array('User');
|
||||
var $helpers = array('Javascript', 'Time', 'Html', 'Form');
|
||||
function beforeFilter() {
|
||||
|
||||
// Find the user that matches the HTTP basic auth user
|
||||
$user = $this->User->find('first', array('recursive' => 0, 'conditions' => array('User.username'=>$_SERVER["PHP_AUTH_USER"])));
|
||||
$this->set("currentuser", $user);
|
||||
|
||||
if($this->RequestHandler->isAjax()) {
|
||||
Configure::write('debug', 0);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if the current logged in user is an admin
|
||||
* @return boolean
|
||||
*/
|
||||
function isAdmin() {
|
||||
$currentuser = $this->getCurrentUser();
|
||||
if($currentuser['access_level'] == 'admin') {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function isManager() {
|
||||
$currentuser = $this->getCurrentUser();
|
||||
if($currentuser['access_level'] == 'manager') {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Read the current logged in user.
|
||||
* @return array - the currently logged in user.
|
||||
*/
|
||||
function getCurrentUser() {
|
||||
$user = $this->User->find('first', array('recursive' => 0, 'conditions' => array('User.username'=>$_SERVER["PHP_AUTH_USER"])));
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the id of the current user. False if not logged in.
|
||||
*/
|
||||
function getCurrentUserID() {
|
||||
$currentuser = $this->getCurrentUser();
|
||||
if($currentuser) {
|
||||
return $currentuser['User']['id'];
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function calculateTotals($document, $gst) {
|
||||
$totals = array('subtotal'=>0, 'gst'=>0, 'total'=>0);
|
||||
|
||||
|
||||
foreach($document['LineItem'] as $lineitem) {
|
||||
if($lineitem['option'] == 1) {
|
||||
$totals['subtotal'] = 'TBA';
|
||||
$totals['total'] = 'TBA';
|
||||
$totals['gst'] = 'TBA';
|
||||
return $totals;
|
||||
}
|
||||
else {
|
||||
$totals['subtotal'] += $lineitem['net_price'];
|
||||
}
|
||||
}
|
||||
|
||||
if($gst == 1) {
|
||||
$totals['gst'] = 0.1*$totals['subtotal'];
|
||||
}
|
||||
$totals['total'] = $totals['gst'] + $totals['subtotal'];
|
||||
return $totals;
|
||||
|
||||
}
|
||||
|
||||
function unset_keys($array, $keys) {
|
||||
foreach($keys as $key ) {
|
||||
$array[$key] = null;
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
|
||||
function unset_multiple_keys($array, $keys) {
|
||||
foreach($array as $index => $item) {
|
||||
$array[$index]['id'] = null;
|
||||
$array[$index]['document_id'] = null;
|
||||
$array[$index]['costing_id'] = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param <type> $year
|
||||
* @param <type> $prevYear
|
||||
* @return <type>
|
||||
*/
|
||||
function getFirstDayFY($year,$prevYear = false) {
|
||||
if($prevYear == false) {
|
||||
return mktime(0,0,0,7,1,$year);
|
||||
|
||||
}
|
||||
else {
|
||||
return mktime(0,0,0,7,1,$year-1);
|
||||
}
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param <type> $year
|
||||
* @return <int>
|
||||
*/
|
||||
function getLastDayFY($year) {
|
||||
return mktime(23,59,59,6,30,$year);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
?>
|
||||
|
|
@ -40,82 +40,19 @@ class AttachmentsController extends AppController {
|
|||
function add() {
|
||||
if (!empty($this->data)) {
|
||||
|
||||
// Check if file was uploaded
|
||||
if (empty($this->data['Attachment']['file']['tmp_name'])) {
|
||||
$error = 'No file uploaded';
|
||||
if (isset($this->data['Attachment']['file']['error'])) {
|
||||
$errorCodes = array(
|
||||
UPLOAD_ERR_INI_SIZE => 'File exceeds upload_max_filesize',
|
||||
UPLOAD_ERR_FORM_SIZE => 'File exceeds MAX_FILE_SIZE',
|
||||
UPLOAD_ERR_PARTIAL => 'File only partially uploaded',
|
||||
UPLOAD_ERR_NO_FILE => 'No file was uploaded',
|
||||
UPLOAD_ERR_NO_TMP_DIR => 'Missing temporary folder',
|
||||
UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk',
|
||||
UPLOAD_ERR_EXTENSION => 'File upload stopped by extension',
|
||||
);
|
||||
$errorCode = $this->data['Attachment']['file']['error'];
|
||||
$error = isset($errorCodes[$errorCode]) ? $errorCodes[$errorCode] : 'Unknown error: ' . $errorCode;
|
||||
}
|
||||
$this->Session->setFlash(__('File upload error: ' . $error, true));
|
||||
$principles = $this->Attachment->Principle->find('list');
|
||||
$this->set(compact('products', 'principles'));
|
||||
return;
|
||||
$attachment = $this->Attachment->process_attachment($this->data);
|
||||
if(!$attachment) {
|
||||
$this->Session->setFlash('The Attachment could not be saved. The filename exists');
|
||||
}
|
||||
else {
|
||||
$this->Attachment->create();
|
||||
|
||||
// Proxy the upload request to the Go application
|
||||
$goHost = getenv('GO_APP_HOST');
|
||||
$goUrl = 'http://' . $goHost . '/go/attachments/upload';
|
||||
|
||||
// Prepare the multipart form data for the Go app
|
||||
$postFields = array();
|
||||
|
||||
$postFields['file'] = new CURLFile(
|
||||
$this->data['Attachment']['file']['tmp_name'],
|
||||
$this->data['Attachment']['file']['type'],
|
||||
$this->data['Attachment']['file']['name']
|
||||
);
|
||||
|
||||
if (!empty($this->data['Attachment']['name'])) {
|
||||
$postFields['name'] = $this->data['Attachment']['name'];
|
||||
}
|
||||
|
||||
if (!empty($this->data['Attachment']['description'])) {
|
||||
$postFields['description'] = $this->data['Attachment']['description'];
|
||||
}
|
||||
|
||||
if (!empty($this->data['Attachment']['principle_id'])) {
|
||||
$postFields['principle_id'] = $this->data['Attachment']['principle_id'];
|
||||
}
|
||||
|
||||
// Make the request to Go app
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $goUrl);
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
|
||||
'Accept: application/json'
|
||||
));
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curlError = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode == 201) {
|
||||
if ($this->Attachment->save($attachment)) {
|
||||
$this->Session->setFlash(__('The Attachment has been saved', true));
|
||||
$this->redirect(array('action'=>'index'));
|
||||
} else {
|
||||
$errorMsg = 'The Attachment could not be saved.';
|
||||
if ($curlError) {
|
||||
$errorMsg .= ' cURL Error: ' . $curlError;
|
||||
} elseif ($response) {
|
||||
$errorMsg .= ' Response: ' . $response;
|
||||
} else {
|
||||
$errorMsg .= ' HTTP Code: ' . $httpCode;
|
||||
$this->Session->setFlash(__('The Attachment could not be saved. Please, try again.', true));
|
||||
}
|
||||
error_log('Attachment upload failed: ' . $errorMsg);
|
||||
$this->Session->setFlash(__($errorMsg, true));
|
||||
}
|
||||
}
|
||||
$principles = $this->Attachment->Principle->find('list');
|
||||
|
|
@ -98,21 +98,6 @@ ENDDETAILS;
|
|||
)
|
||||
));
|
||||
|
||||
$sql = "SELECT DISTINCT freight_forwarder_text FROM purchase_orders WHERE freight_forwarder_text != '' ORDER BY freight_forwarder_text ASC;";
|
||||
|
||||
$previousValues = $this->Document->query($sql);
|
||||
$ffOptions = array(
|
||||
"CMC will make the booking with Fedex Economy. We will send you the labels. Please send us the packing list, invoice, weights and dimensions.",
|
||||
"FEDEX Airfreight Economy service on CMC Account #409368441",
|
||||
"FEDEX INTERNATIONAL Priority on CMC Account #409368441"
|
||||
);
|
||||
|
||||
foreach($previousValues as $val) {
|
||||
array_push($ffOptions, $val["purchase_orders"]["freight_forwarder_text"]);
|
||||
}
|
||||
|
||||
asort($ffOptions);
|
||||
$this->set('freightForwarderSelect', $ffOptions);
|
||||
break;
|
||||
case 'orderAck':
|
||||
$this->set('currencies', $this->Document->OrderAcknowledgement->Currency->find('list'));
|
||||
|
|
@ -314,16 +299,13 @@ ENDDETAILS;
|
|||
$this->data['PurchaseOrder']['title'] = $newPOnumber;
|
||||
$this->data['PurchaseOrder']['issue_date'] = date('Y-m-d');
|
||||
|
||||
$this->data['PurchaseOrder']['description'] = "Items as per attached schedule";
|
||||
$this->data['PurchaseOrder']['deliver_to'] = <<<ENDDELIVER
|
||||
CMC TECHNOLOGIES PTY LTD<br>
|
||||
Unit 19, 77 Bourke Road<br>
|
||||
Alexandria NSW 2015 AUSTRALIA<br>
|
||||
Attn: Con Carpis E: sales@cmctechnologies.com.au<br>
|
||||
P: +61 2 9669 4000 F:+61 2 9669 4111
|
||||
ENDDELIVER;
|
||||
|
||||
|
||||
//Fuck it all.
|
||||
$this->data['PurchaseOrder']['shipping_instructions'] = <<<ENDINSTRUCTIONS
|
||||
|
||||
<b>PART SHIPMENT:</b> Not Acceptable - please advise ASAP if production of an Item will Delay the Dispatch of the Complete Order by the Date Required stated above.<br>
|
||||
|
|
@ -389,6 +371,7 @@ ENDINSTRUCTIONS;
|
|||
|
||||
|
||||
|
||||
|
||||
if(isset($source_document_id)) {
|
||||
//This is not ideal. But nothing else is either.
|
||||
$sourceDoc = $this->Document->find('first', array('conditions' => array('Document.id' => $source_document_id)));
|
||||
|
|
@ -444,19 +427,6 @@ ENDINSTRUCTIONS;
|
|||
|
||||
}
|
||||
|
||||
function getBaseTitle($titleWithRevision) {
|
||||
// The pattern matches:
|
||||
// - : the literal hyphen
|
||||
// Rev : the literal string "Rev" (case-sensitive)
|
||||
// \d+ : one or more digits (representing the revision number)
|
||||
// $ : asserts that this pattern must be at the end of the string
|
||||
//
|
||||
// If "Rev" could be "rev", "REV", etc., you can make it case-insensitive
|
||||
// by adding the 'i' flag: '/-Rev\d+$/i'
|
||||
$baseTitle = preg_replace('/-Rev\d+$/', '', $titleWithRevision);
|
||||
return $baseTitle;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Revise a Document.
|
||||
|
|
@ -477,48 +447,7 @@ ENDINSTRUCTIONS;
|
|||
$this->Document->create();
|
||||
|
||||
if(!empty($document['Invoice']['id'])) {
|
||||
// Invoice revision
|
||||
$newDoc = array();
|
||||
|
||||
// 1. Copy Document fields and update revision
|
||||
$newDoc['Document'] = $this->unset_keys($document['Document'], array('id', 'created', 'modified'));
|
||||
$newDoc['Document']['revision'] = $document['Document']['revision'] + 1;
|
||||
// user_id for the new revision will be the current user
|
||||
$currentUser = $this->GetCurrentUser(); // Assumes GetCurrentUser() is available
|
||||
$newDoc['Document']['user_id'] = $currentUser['User']['id'];
|
||||
// CakePHP will set created/modified timestamps automatically for the new record
|
||||
|
||||
// 2. Copy Invoice fields
|
||||
$newDoc['Invoice'] = $this->unset_keys($document['Invoice'], array('id', 'document_id', 'created', 'modified'));
|
||||
$newDoc['Invoice']['issue_date'] = date('Y-m-d');
|
||||
$newDoc['Invoice']['due_date'] = date("Y-m-d", strtotime("+30 days"));
|
||||
|
||||
// Modify title for uniqueness, appending revision number
|
||||
$orginalTitleParts = explode('-', $document['Invoice']['title']);
|
||||
$newDoc['Invoice']['title'] = $orginalTitleParts[0] . '-REV' . $newDoc['Document']['revision'];
|
||||
|
||||
// 3. Copy DocPage records (if any)
|
||||
if (!empty($document['DocPage'])) {
|
||||
$newDoc['DocPage'] = $document['DocPage'];
|
||||
foreach ($newDoc['DocPage'] as $index => $page) {
|
||||
$newDoc['DocPage'][$index]['id'] = null; // New record
|
||||
$newDoc['DocPage'][$index]['document_id'] = null; // Will be set by saveAll
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Copy Job associations (if applicable - check your data model)
|
||||
if (!empty($document['Job'])) {
|
||||
$job_ids = array();
|
||||
foreach ($document['Job'] as $job) {
|
||||
$job_ids[] = $job['id'];
|
||||
}
|
||||
// This structure is typically used by saveAll for HABTM relationships
|
||||
$newDoc['Job']['Job'] = $job_ids;
|
||||
}
|
||||
|
||||
// Store info for flash message
|
||||
// $this->set('revision_number_for_flash', $newDoc['Document']['revision']);
|
||||
// $this->set('document_type_for_flash', 'Invoice');
|
||||
echo "WE HAVE AN INVOICE";
|
||||
}
|
||||
else if (!empty($document['Quote']['id'])) {
|
||||
|
||||
|
|
@ -541,10 +470,6 @@ ENDINSTRUCTIONS;
|
|||
$newDoc['Document']['revision'] = $number_of_revisions;
|
||||
$newDoc['Document']['type'] = 'quote';
|
||||
|
||||
// user_id for the new revision will be the current user
|
||||
$currentUser = $this->GetCurrentUser();
|
||||
$newDoc['Document']['user_id'] = $currentUser['User']['id'];
|
||||
|
||||
|
||||
$newDoc['DocPage'] = $document['DocPage'];
|
||||
|
||||
|
|
@ -556,55 +481,12 @@ ENDINSTRUCTIONS;
|
|||
}
|
||||
|
||||
else if (!empty($document['PurchaseOrder']['id'])) {
|
||||
// Purchase Order revision
|
||||
$newDoc = array();
|
||||
|
||||
// 1. Copy Document fields and update revision
|
||||
$newDoc['Document'] = $this->unset_keys($document['Document'], array('id', 'created', 'modified'));
|
||||
$newDoc['Document']['revision'] = $document['Document']['revision'] + 1;
|
||||
$newDoc['Document']['type'] = 'purchaseOrder'; // Ensure type is set
|
||||
|
||||
// user_id for the new revision will be the current user
|
||||
$currentUser = $this->GetCurrentUser();
|
||||
$newDoc['Document']['user_id'] = $currentUser['User']['id'];
|
||||
|
||||
// 2. Copy PurchaseOrder fields
|
||||
$newDoc['PurchaseOrder'] = $this->unset_keys($document['PurchaseOrder'], array('id', 'document_id', 'created', 'modified'));
|
||||
$newDoc['PurchaseOrder']['issue_date'] = date('Y-m-d');
|
||||
|
||||
// Modify title for uniqueness, appending revision number
|
||||
$baseTitle = $this->getBaseTitle($document['PurchaseOrder']['title']);
|
||||
$newDoc['PurchaseOrder']['title'] = $baseTitle . '-Rev' . $newDoc['Document']['revision'];
|
||||
|
||||
// 3. Copy DocPage records (if any)
|
||||
if (!empty($document['DocPage'])) {
|
||||
$newDoc['DocPage'] = array();
|
||||
foreach ($document['DocPage'] as $page) {
|
||||
$newPage = $this->unset_keys($page, array('id', 'document_id', 'created', 'modified'));
|
||||
$newDoc['DocPage'][] = $newPage;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Handle Job associations (HABTM)
|
||||
// First, we need to fetch the jobs associated with the original purchase order
|
||||
$originalPO = $this->Document->PurchaseOrder->find('first', array(
|
||||
'conditions' => array('PurchaseOrder.id' => $document['PurchaseOrder']['id']),
|
||||
'contain' => array('Job')
|
||||
));
|
||||
|
||||
if (!empty($originalPO['Job'])) {
|
||||
$job_ids = array();
|
||||
foreach ($originalPO['Job'] as $job) {
|
||||
$job_ids[] = $job['id'];
|
||||
}
|
||||
// Store job IDs to be processed after the main save
|
||||
$newDoc['_job_ids'] = $job_ids;
|
||||
}
|
||||
echo "WE ARE REVISING A PO";
|
||||
}
|
||||
|
||||
|
||||
else if (!empty($document['OrderAcknowledgement']['id'])) {
|
||||
//TODO Order Acknowledgement revision
|
||||
echo "WE ARE REVISING An ORDER ACK";
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -619,39 +501,7 @@ ENDINSTRUCTIONS;
|
|||
|
||||
if ($this->Document->saveAll($newDoc)) {
|
||||
$newid = $this->Document->id;
|
||||
|
||||
// Handle Purchase Order Job associations
|
||||
if (!empty($document['PurchaseOrder']['id']) && !empty($newDoc['_job_ids'])) {
|
||||
// Get the new PurchaseOrder ID
|
||||
$newPurchaseOrder = $this->Document->PurchaseOrder->find('first', array(
|
||||
'conditions' => array('PurchaseOrder.document_id' => $newid),
|
||||
'fields' => array('PurchaseOrder.id')
|
||||
));
|
||||
|
||||
if (!empty($newPurchaseOrder['PurchaseOrder']['id'])) {
|
||||
$po_id = $newPurchaseOrder['PurchaseOrder']['id'];
|
||||
|
||||
// Method 1: Using CakePHP's HABTM save (recommended)
|
||||
$this->Document->PurchaseOrder->id = $po_id;
|
||||
|
||||
// Method 2: If Method 1 doesn't work, use the SQL approach as a fallback
|
||||
foreach($newDoc['_job_ids'] as $job_id) {
|
||||
$query = "INSERT INTO `jobs_purchase_orders` (`job_id`, `purchase_order_id`) VALUES ('{$job_id}', '{$po_id}');";
|
||||
$this->Document->query($query);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Updated flash message logic
|
||||
$revisionNumber = $newDoc['Document']['revision'];
|
||||
$docTypeFullName = 'Unknown Document';
|
||||
if (isset($newDoc['Document']['type'])) {
|
||||
$docTypeFullName = $this->Document->getDocFullName($newDoc['Document']['type']);
|
||||
} else if (isset($document['Document']['type'])) {
|
||||
$docTypeFullName = $this->Document->getDocFullName($document['Document']['type']);
|
||||
}
|
||||
|
||||
$this->Session->setFlash(__("Revision {$revisionNumber} of {$docTypeFullName} created", true));
|
||||
$this->Session->setFlash(__("Revision {$number_of_revisions} created", true));
|
||||
$this->redirect(array('action'=>'view',$newid));
|
||||
} else {
|
||||
$this->Session->setFlash(__('The Document could not be saved. Please, try again.', true));
|
||||
|
|
@ -676,7 +526,8 @@ ENDINSTRUCTIONS;
|
|||
//Delete all the existing JobPurchaseOrder relationships for this PO.
|
||||
//Fuck it. @TODO SQL injection potential here.
|
||||
$query = "DELETE FROM jobs_purchase_orders WHERE purchase_order_id =".$this->data['PurchaseOrder']['id'];
|
||||
$this->Document->query($query);
|
||||
$result = $this->Document->query($query);
|
||||
|
||||
}
|
||||
|
||||
if(isset($this->data['PurchaseOrder']['Job'])) {
|
||||
|
|
@ -1116,18 +967,21 @@ EOT;
|
|||
$this->Email->delivery = 'smtp';
|
||||
|
||||
$document = $this->Document->read(null,$id);
|
||||
|
||||
if(empty($document['Document']['pdf_filename'])) {
|
||||
$this->Session->setFlash(__('Error. Please generate the PDF before attempting to email it', true));
|
||||
return;
|
||||
}
|
||||
else {
|
||||
$pdf_dir = Configure::read('pdf_directory');
|
||||
|
||||
$attachment_files = array($pdf_dir.$document['Document']['pdf_filename']);
|
||||
foreach($document['DocumentAttachment'] as $document_attachment) {
|
||||
$attachment = $this->Document->DocumentAttachment->Attachment->read(null, $document_attachment['attachment_id']);
|
||||
$attachment_files[] = $attachment['Attachment']['file'];
|
||||
}
|
||||
$this->Email->attachments = $attachment_files;
|
||||
|
||||
}
|
||||
$enquiry = $this->Document->getEnquiry($document);
|
||||
|
||||
|
|
@ -1206,285 +1060,6 @@ EOT;
|
|||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function format_email($email) {
|
||||
$email = trim($email);
|
||||
// Basic RFC 5322 email validation
|
||||
if (!preg_match('/^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/', $email)) {
|
||||
return '';
|
||||
}
|
||||
return $email;
|
||||
}
|
||||
|
||||
function parse_email_to_array($input) {
|
||||
try {
|
||||
if (empty($input) || !is_string($input)) {
|
||||
return array();
|
||||
}
|
||||
$input = trim($input);
|
||||
if ($input === '') {
|
||||
return array();
|
||||
}
|
||||
if (strpos($input, ',') !== false) {
|
||||
$parts = explode(',', $input);
|
||||
$result = array();
|
||||
foreach ($parts as $email) {
|
||||
$email = $this->format_email($email);
|
||||
if ($email !== '') {
|
||||
$result[] = $email;
|
||||
}
|
||||
}
|
||||
return $result;
|
||||
} else {
|
||||
$result = $this->format_email($input);
|
||||
$array[] = $result;
|
||||
return $array;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
return array();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Email the PDF(document + attachments) for this Document to custom recipients.
|
||||
*
|
||||
* @param int $id - Document ID
|
||||
* @param string $to - Recipient email address (comma-separated if multiple)
|
||||
* @param string|null $cc - CC email address(es), optional (comma-separated)
|
||||
* @param string|null $bcc - BCC email address(es), optional (comma-separated)
|
||||
*/
|
||||
function email_pdf_with_custom_recipients($id = null, $to = null, $cc = null, $bcc = null) {
|
||||
// Disable automatic rendering of a view
|
||||
$this->autoRender = false;
|
||||
|
||||
// Retrieve recipient emails from form data if not provided as arguments
|
||||
if (empty($to) && !empty($this->params['form']['to'])) {
|
||||
$to = $this->params['form']['to'];
|
||||
}
|
||||
if (empty($cc) && isset($this->params['form']['cc'])) {
|
||||
$cc = $this->params['form']['cc'];
|
||||
}
|
||||
if (empty($bcc) && isset($this->params['form']['bcc'])) {
|
||||
$bcc = $this->params['form']['bcc'];
|
||||
}
|
||||
|
||||
// Basic validation for required parameters
|
||||
if (empty($id) || empty($to)) {
|
||||
$msg = 'Document ID and recipient email are required.';
|
||||
echo json_encode(array('success' => false, 'message' => $msg));
|
||||
return;
|
||||
}
|
||||
|
||||
// Configure SMTP settings for email delivery
|
||||
$this->Email->smtpOptions = Configure::read('smtp_settings');
|
||||
$this->Email->delivery = 'smtp';
|
||||
|
||||
// Load the document and its attachments
|
||||
$document = $this->Document->read(null, $id);
|
||||
error_log("[email_pdf_with_custom_recipients] Document loaded: " . print_r($document['Document'], true));
|
||||
error_log("[email_pdf_with_custom_recipients] DocumentAttachments: " . print_r($document['DocumentAttachment'], true));
|
||||
if (empty($document) || empty($document['Document'])) {
|
||||
$msg = 'Document not found.';
|
||||
echo json_encode(array('success' => false, 'message' => $msg));
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure the PDF has been generated before emailing
|
||||
if (empty($document['Document']['pdf_filename'])) {
|
||||
$msg = 'Error. Please generate the PDF before attempting to email it';
|
||||
echo json_encode(array('success' => false, 'message' => $msg));
|
||||
return;
|
||||
}
|
||||
|
||||
// Build the list of attachments (PDF + any additional attachments)
|
||||
$pdf_dir = Configure::read('pdf_directory');
|
||||
$attachment_files = array($pdf_dir.$document['Document']['pdf_filename']);
|
||||
foreach($document['DocumentAttachment'] as $document_attachment) {
|
||||
$attachment = $this->Document->DocumentAttachment->Attachment->read(null, $document_attachment['attachment_id']);
|
||||
$attachment_files[] = $attachment['Attachment']['file'];
|
||||
error_log("[email_pdf_with_custom_recipients] Added attachment: " . $attachment['Attachment']['file']);
|
||||
}
|
||||
if (!empty($document['DocumentAttachment'])) {
|
||||
foreach ($document['DocumentAttachment'] as $document_attachment) {
|
||||
if (!empty($document_attachment['attachment_id'])) {
|
||||
$attachment = $this->Document->DocumentAttachment->Attachment->read(null, $document_attachment['attachment_id']);
|
||||
if (!empty($attachment['Attachment']['file'])) {
|
||||
$attachment_files[] = $attachment['Attachment']['file'];
|
||||
error_log("[email_pdf_with_custom_recipients] Added attachment: " . $attachment['Attachment']['file']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
error_log("[email_pdf_with_custom_recipients] All attachments: " . print_r($attachment_files, true));
|
||||
$this->Email->attachments = $attachment_files;
|
||||
|
||||
// Get related enquiry for the document
|
||||
$enquiry = $this->Document->getEnquiry($document);
|
||||
|
||||
// Parse and validate recipient email addresses
|
||||
$toArray = $this->parse_email_to_array($to);
|
||||
if (empty($toArray)) {
|
||||
$msg = 'Invalid recipient email address.';
|
||||
echo json_encode(array('success' => false, 'message' => $msg));
|
||||
return;
|
||||
} else {
|
||||
// Pass as array - EmailComponent now properly handles arrays for TO field
|
||||
$this->Email->to = $toArray;
|
||||
}
|
||||
$ccArray = $this->parse_email_to_array($cc);
|
||||
if (!empty($ccArray)) {
|
||||
$this->Email->cc = $ccArray;
|
||||
}
|
||||
$bccArray = $this->parse_email_to_array($bcc);
|
||||
// Add always BCC recipients
|
||||
// These emails will always be included in the BCC list, regardless of user input
|
||||
$alwaysBcc = array('<carpis@cmctechnologies.com.au>', '<mcarpis@cmctechnologies.com.au>');
|
||||
foreach ($alwaysBcc as $bccEmail) {
|
||||
if (!in_array($bccEmail, $bccArray)) {
|
||||
$bccArray[] = $bccEmail;
|
||||
}
|
||||
}
|
||||
if (!empty($bccArray)) {
|
||||
$this->Email->bcc = $bccArray;
|
||||
}
|
||||
|
||||
// Set reply-to and from addresses
|
||||
$this->Email->replyTo = 'CMC Technologies - Sales <sales@cmctechnologies.com.au>';
|
||||
$this->Email->from = 'CMC Technologies - Sales <sales@cmctechnologies.com.au>';
|
||||
|
||||
// Determine document type, email template, and subject
|
||||
$docType = $this->Document->getDocType($document);
|
||||
$template = $docType . '_email';
|
||||
$subject = !empty($enquiry['Enquiry']['title']) ? $enquiry['Enquiry']['title'] . ' ' : 'Document';
|
||||
error_log("[email_pdf_with_custom_recipients] Enquiry Title: " . empty($enquiry['Enquiry']['title']) . $enquiry['Enquiry']['title']);
|
||||
|
||||
// Customise subject and template based on document type
|
||||
switch($docType) {
|
||||
case 'quote':
|
||||
$subject = !empty($enquiry['Enquiry']['title']) ? "Quotation: " . $enquiry['Enquiry']['title'] : 'Quotation';
|
||||
break;
|
||||
case 'invoice':
|
||||
$subject = $this->invoice_email_subject($document);
|
||||
if (!empty($document['Invoice']['id'])) {
|
||||
$this->set('invoice', $this->Document->Invoice->find('first', array('conditions'=>array('Invoice.id'=>$document['Invoice']['id']))));
|
||||
}
|
||||
if (!empty($document['Invoice']['job_id'])) {
|
||||
$this->set('job', $this->Document->Invoice->Job->find('first', array('conditions'=>array('Job.id'=>$document['Invoice']['job_id']))));
|
||||
}
|
||||
break;
|
||||
case 'purchaseOrder':
|
||||
$subject .= "Purchase Order";
|
||||
$primary_contact = null;
|
||||
if (!empty($document['PurchaseOrder']['principle_id'])) {
|
||||
$primary_contact = $this->Document->User->find('first', array('conditions'=>array('User.principle_id' => $document['PurchaseOrder']['principle_id'],'User.primary_contact' => 1)));
|
||||
}
|
||||
if(empty($primary_contact)) {
|
||||
$msg = 'Unable to send. No primary Principle Contact';
|
||||
echo json_encode(array('success' => false, 'message' => $msg));
|
||||
return;
|
||||
}
|
||||
$subject = $this->po_email_subject($document['PurchaseOrder']);
|
||||
if (!empty($document['OrderAcknowledgement']['job_id'])) {
|
||||
$this->set('job', $this->Document->PurchaseOrder->Job->find('first', array('conditions'=>array('Job.id'=>$document['OrderAcknowledgement']['job_id']))));
|
||||
}
|
||||
break;
|
||||
case 'orderAck':
|
||||
$subject = $this->orderack_email_subject($document);
|
||||
if (!empty($document['OrderAcknowledgement']['job_id'])) {
|
||||
$this->set('job', $this->Document->OrderAcknowledgement->Job->find('first', array('conditions'=>array('Job.id'=>$document['OrderAcknowledgement']['job_id']))));
|
||||
}
|
||||
if (!empty($document['OrderAcknowledgement']['signature_required'])) {
|
||||
$template = 'orderAck_email_signature_required';
|
||||
}
|
||||
break;
|
||||
case 'packingList':
|
||||
$subject = $this->packing_list_email_subject($document);
|
||||
break;
|
||||
}
|
||||
|
||||
// Set email template and other parameters
|
||||
$this->Email->template = $template;
|
||||
$this->Email->subject = $subject;
|
||||
$this->Email->sendAs = 'both';
|
||||
$this->Email->charset = 'iso-8859-1';
|
||||
$this->set('enquiry', $enquiry);
|
||||
$this->set('document', $document);
|
||||
$this->set('DocFullName', $this->Document->getDocFullName($document['Document']['type']));
|
||||
|
||||
// Attempt to send the email and handle errors
|
||||
$sent = false;
|
||||
try {
|
||||
$sent = $this->Email->send();
|
||||
} catch (Exception $e) {
|
||||
$msg = 'Exception: ' . $e->getMessage();
|
||||
echo json_encode(array('success' => false, 'message' => $msg));
|
||||
return;
|
||||
}
|
||||
if ($sent) {
|
||||
echo json_encode(array('success' => true, 'message' => 'The Email has been sent'));
|
||||
return;
|
||||
} else {
|
||||
$msg = 'The Email has NOT been sent';
|
||||
echo json_encode(array('success' => false, 'message' => $msg, 'smtp_errors' => $this->Email->smtpError));
|
||||
return;
|
||||
}
|
||||
echo json_encode(array('success' => false, 'message' => 'No response from email function'));
|
||||
}
|
||||
|
||||
// generateShippingReference builds the Shipping Instructions: with the PO number and job titles.
|
||||
function generateShippingInstructions($document_id) {
|
||||
$this->layout = 'ajax';
|
||||
|
||||
$po = $this->Document->PurchaseOrder->find('first',
|
||||
array('conditions'=>
|
||||
array('Document.id' => $document_id)
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
// TODO SQL injection risk. Need to rewrite this whole rickety thing..
|
||||
$query = 'select j.title as job_title, po.title as po_title from jobs_purchase_orders jpo JOIN jobs j on j.id = jpo.job_id JOIN purchase_orders po ON po.id= jpo.purchase_order_id WHERE jpo.purchase_order_id = '.$po['PurchaseOrder']['id'].';';
|
||||
|
||||
|
||||
|
||||
$results = $this->Document->query($query);
|
||||
//echo "<pre>";
|
||||
//print_r($results);
|
||||
//echo "</pre>";
|
||||
|
||||
$label = "";
|
||||
|
||||
if (count($results) > 0) {
|
||||
$label = $results[0]["po"]["po_title"];
|
||||
}
|
||||
|
||||
foreach ($results as $result) {
|
||||
$label .= " / ". $result["j"]["job_title"];
|
||||
}
|
||||
$ffText = $po['PurchaseOrder']['freight_forwarder_text'];
|
||||
echo <<<ENDINSTRUCTIONS
|
||||
<b>FREIGHT FORWARDER:</b> $ffText<br>
|
||||
<b>PART SHIPMENT:</b> Not Acceptable - please advise ASAP if production of an Item will Delay the Dispatch of the Complete Order by the Date Required stated above.<br>
|
||||
<b>INSURANCE:</b> DO NOT INSURE - Insurance effected by CMC<br>
|
||||
<b>SECURITY:</b> It is essential that the cargo is prepared & handled so as not to compromise its security standing.<br>
|
||||
<b>SHIPMENT MARKINGS:</b><br>
|
||||
(Please put red fragile stickers on boxing)<br>
|
||||
CMC TECHNOLOGIES PTY LTD<br>
|
||||
UNIT 19, 77 BOURKE ROAD<br>
|
||||
ALEXANDRIA NSW 2015 AUSTRALIA<br>
|
||||
REF: $label
|
||||
ENDINSTRUCTIONS;
|
||||
|
||||
/*if($this->Document->PurchaseOrder->save($po["PurchaseOrder"])) {
|
||||
echo "SUCCESS"; //THIS IS SO STUPID.
|
||||
}
|
||||
else {
|
||||
echo "FAILURE";
|
||||
}*/
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
function add_job_items_to_po($job_id, $document_id) {
|
||||
|
|
@ -1554,4 +1129,5 @@ ENDINSTRUCTIONS;
|
|||
}
|
||||
return $newDoc[$model];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -15,17 +15,8 @@ class EmailAttachmentsController extends AppController {
|
|||
|
||||
$file = $this->EmailAttachment->findById($id);
|
||||
|
||||
// Try legacy emails directory first (where vault saves files)
|
||||
$file_path = '/var/www/emails';
|
||||
$full_path = $file_path."/".$file['EmailAttachment']['name'];
|
||||
|
||||
// Fallback to attachments directory if not found in emails
|
||||
if(!file_exists($full_path)) {
|
||||
$file_path = Configure::read('attachments_directory');
|
||||
$full_path = $file_path."/".$file['EmailAttachment']['name'];
|
||||
}
|
||||
|
||||
if(file_exists($full_path)) {
|
||||
$file_path = Configure::read('email_directory');
|
||||
if(file_exists($file_path."/".$file['EmailAttachment']['name'])) {
|
||||
|
||||
|
||||
|
||||
|
|
@ -42,13 +33,12 @@ class EmailAttachmentsController extends AppController {
|
|||
header('Content-length: ' . $file['EmailAttachment']['size']);
|
||||
header('Content-Disposition: attachment; filename='.$filename);
|
||||
|
||||
readfile($full_path);
|
||||
readfile($file_path."/".$file['EmailAttachment']['name']);
|
||||
|
||||
exit();
|
||||
}
|
||||
else {
|
||||
echo "ERROR: File Not Found";
|
||||
echo '\n';
|
||||
echo "ERROR!! : File Not Found";
|
||||
echo $file['EmailAttachment']['filename'];
|
||||
die();
|
||||
}
|
||||
|
|
@ -65,17 +55,9 @@ class EmailAttachmentsController extends AppController {
|
|||
$file = $this->EmailAttachment->find('first', array('conditions'=>array('EmailAttachment.id'=>$id)));
|
||||
//$this->set('attachment', $file);
|
||||
|
||||
// Try legacy emails directory first (where vault saves files)
|
||||
$file_path = '/var/www/emails';
|
||||
$full_path = $file_path."/".$file['EmailAttachment']['name'];
|
||||
$file_path = Configure::read('email_directory');
|
||||
|
||||
// Fallback to attachments directory if not found in emails
|
||||
if(!file_exists($full_path)) {
|
||||
$file_path = Configure::read('attachments_directory');
|
||||
$full_path = $file_path."/".$file['EmailAttachment']['name'];
|
||||
}
|
||||
|
||||
$contents = file_get_contents($full_path);
|
||||
$contents = file_get_contents($file_path."/".$file['EmailAttachment']['name']);
|
||||
|
||||
|
||||
if($file['EmailAttachment']['type'] == 'text/plain') {
|
||||
|
|
@ -5,7 +5,7 @@ class PrinciplesController extends AppController {
|
|||
var $helpers = array('Html', 'Form');
|
||||
|
||||
var $paginate = array(
|
||||
'Principle' => array('limit' => 500, 'order' => array('Principle.name' => 'asc')),
|
||||
'Principle' => array('limit' => 50, 'order' => array('Principle.name' => 'asc')),
|
||||
'Enquiry' => array('limit' => 150, 'order' => array('Enquiry.id' => 'desc'))
|
||||
);
|
||||
|
||||
|
|
@ -13,7 +13,6 @@ class PrinciplesController extends AppController {
|
|||
$this->Principle->recursive = 0;
|
||||
$this->set('principles', $this->paginate());
|
||||
$this->set('enquiries', $this->paginate());
|
||||
|
||||
}
|
||||
|
||||
function view($id = null) {
|
||||
|
|
@ -25,14 +24,14 @@ class PrinciplesController extends AppController {
|
|||
$this->set('enquiries', $this->paginate('Enquiry', array('Enquiry.principle_id' => $id)));
|
||||
$this->set('addresses', $this->Principle->PrincipleAddress->findAllByPrincipleId($id));
|
||||
$this->set('principleContacts', $this->Principle->PrincipleContact->find('all', array('conditions'=>array('PrincipleContact.principle_id'=>$id))));
|
||||
|
||||
$status_list = $this->Principle->Enquiry->Status->getJSON();
|
||||
$statuses = $this->Principle->Enquiry->Status->find('all');
|
||||
$status_list = array();
|
||||
foreach ($statuses as $status) {
|
||||
$status_list[] = array($status['Status']['id'], $status['Status']['name']);
|
||||
}
|
||||
$this->set('status_list', $status_list);
|
||||
|
||||
$classNames = $this->Principle->Enquiry->Status->getClassNamesJSON();
|
||||
$this->set('class_names', $classNames);
|
||||
|
||||
$this->set('enquiries', $this->paginate('Enquiry', array('Enquiry.principle_id' => $id)));
|
||||
|
||||
}
|
||||
|
||||
function add() {
|
||||
|
|
@ -72,15 +71,5 @@ class PrinciplesController extends AppController {
|
|||
}
|
||||
|
||||
|
||||
function defaults($id = null) {
|
||||
$this->layout = 'ajax';
|
||||
if($id == null) {
|
||||
echo("No ID");
|
||||
}
|
||||
$principle = $this->Principle->read(null, $id);
|
||||
$principle_json = json_encode($principle);
|
||||
$this->set('principle_json', $principle_json);
|
||||
}
|
||||
|
||||
}
|
||||
?>
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue