feat: migrate to ERPNext #20

Merged
Alchez merged 8 commits from feat-migrate into main 2025-06-04 22:33:21 +00:00
79 changed files with 1345 additions and 1692 deletions

View file

@ -1,15 +1,14 @@
exclude: 'node_modules|.git'
default_stages: [commit]
default_stages: [pre-commit]
fail_fast: false
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
rev: v5.0.0
hooks:
- id: trailing-whitespace
files: "suppliers_portal.*"
exclude: ".*json$|.*txt$|.*csv|.*md|.*svg"
files: 'suppliers_portal.*'
exclude: '.*json$|.*txt$|.*csv|.*md|.*svg'
- id: check-yaml
- id: check-merge-conflict
- id: check-ast
@ -19,49 +18,41 @@ repos:
- id: debug-statements
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.2.0
rev: v0.11.7
hooks:
- id: ruff
name: "Run ruff linter and apply fixes"
args: ["--fix"]
name: 'Run ruff linter and apply fixes'
args: ['--fix']
- id: ruff-format
name: "Format Python code"
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.7.1
hooks:
- id: prettier
types_or: [javascript, vue, scss]
# Ignore any files that might contain jinja / bundles
exclude: |
(?x)^(
suppliers_portal/public/dist/.*|
.*node_modules.*|
.*boilerplate.*|
suppliers_portal/templates/includes/.*|
suppliers_portal/public/js/lib/.*
)$
name: 'Format Python code'
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v8.44.0
rev: v8.56.0
hooks:
- id: eslint
types_or: [javascript]
args: ['--quiet']
# Ignore any files that might contain jinja / bundles
exclude: |
(?x)^(
suppliers_portal/public/dist/.*|
cypress/.*|
.*node_modules.*|
.*boilerplate.*|
suppliers_portal/templates/includes/.*|
suppliers_portal/public/js/lib/.*
)$
(?x)^(
.*boilerplate.*|
.*node_modules.*|
cypress/.*|
suppliers_portal/public/dist/.*|
suppliers_portal/public/js/lib/.*|
suppliers_portal/public/supplier-portal/assets/.*|
suppliers_portal/templates/includes/.*
)$
- repo: local
hooks:
- id: prettier
name: prettier
entry: npx prettier -w . --config .prettierrc.cjs --ignore-path .prettierignore
language: system
ci:
autoupdate_schedule: weekly
skip: []
submodules: false
autoupdate_schedule: weekly
skip: []
submodules: false

222
.prettierignore Normal file
View file

@ -0,0 +1,222 @@
#-------------------------------------------------------------------------------------------------------------------
# Keep this section in sync with .gitignore
#-------------------------------------------------------------------------------------------------------------------
*.pyc
*.py~
*.comp.js
*.DS_Store
locale
.wnf-lang-status
*.swp
*.egg-info
dist/
# build/
suppliers_portal/docs/current
suppliers_portal/public/dist
suppliers_portal/public/supplier-portal/assets
.vscode
.vs
node_modules
.kdev4/
*.kdev4
*debug.log
# Not Recommended, but will remove once webpack ready
package-lock.json
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
# build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
.cypress-coverage
# Translations
*.mo
*.pot
# Django stuff:
*.log
.static_storage/
.media/
local_settings.py
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# next.js build output
.next
# cypress
cypress/screenshots
cypress/videos
# JetBrains IDEs
.idea/
#-------------------------------------------------------------------------------------------------------------------
# Prettier-specific overrides
#-------------------------------------------------------------------------------------------------------------------
# Package manager files
pnpm-lock.yaml
yarn.lock
package-lock.json
shrinkwrap.json
# Build outputs
lib
.github
# Prettier reformats code blocks inside Markdown, which affects rendered output
*.md
# In a Frappe context HTML files are either Jinja or Resig's microtemplate format and should not be checked
*.html

19
.prettierrc.cjs Normal file
View file

@ -0,0 +1,19 @@
module.exports = {
arrowParens: 'avoid',
bracketSameLine: true,
bracketSpacing: true,
embeddedLanguageFormatting: 'auto',
htmlWhitespaceSensitivity: 'css',
insertPragma: false,
jsxSingleQuote: false,
printWidth: 120,
proseWrap: 'preserve',
quoteProps: 'as-needed',
requirePragma: false,
semi: false,
singleQuote: true,
tabWidth: 2,
trailingComma: 'es5',
useTabs: true,
vueIndentScriptAndStyle: false,
}

View file

@ -1,4 +1,4 @@
{
"semi": false,
"singleQuote": true
"semi": false,
"singleQuote": true
}

View file

@ -1,26 +1,26 @@
{
"name": "frappe-ui-frontend",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build --base=/assets/suppliers_portal/supplier-portal/ && yarn copy-html-entry",
"preview": "vite preview",
"copy-html-entry": "cp ../suppliers_portal/public/supplier-portal/index.html ../suppliers_portal/www/supplier-portal.html"
},
"dependencies": {
"bootstrap": "5.2.3",
"bootstrap-icons": "^1.11.3",
"feather-icons": "^4.28.0",
"frappe-ui": "^0.1.69",
"vue": "^3.2.25",
"vue-router": "^4.0.12"
},
"devDependencies": {
"@vitejs/plugin-vue": "^2.0.0",
"autoprefixer": "^10.4.2",
"postcss": "^8.4.5",
"tailwindcss": "^3.0.15",
"vite": "^2.7.2"
}
"name": "frappe-ui-frontend",
"private": true,
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build --base=/assets/suppliers_portal/supplier-portal/ && yarn copy-html-entry",
"preview": "vite preview",
"copy-html-entry": "cp ../suppliers_portal/public/supplier-portal/index.html ../suppliers_portal/www/supplier-portal.html"
},
"dependencies": {
"bootstrap": "5.2.3",
"bootstrap-icons": "^1.11.3",
"feather-icons": "^4.28.0",
"frappe-ui": "^0.1.69",
"vue": "^3.2.25",
"vue-router": "^4.0.12"
},
"devDependencies": {
"@vitejs/plugin-vue": "^2.0.0",
"autoprefixer": "^10.4.2",
"postcss": "^8.4.5",
"tailwindcss": "^3.0.15",
"vite": "^2.7.2"
}
}

View file

@ -1,6 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View file

@ -1,5 +1,5 @@
<template>
<div>
<router-view />
</div>
<div>
<router-view />
</div>
</template>

View file

@ -1,152 +1,170 @@
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 100;
font-display: swap;
src: url("Inter-Thin.woff2?v=3.12") format("woff2"),
url("Inter-Thin.woff?v=3.12") format("woff");
font-family: 'Inter';
font-style: normal;
font-weight: 100;
font-display: swap;
src:
url('Inter-Thin.woff2?v=3.12') format('woff2'),
url('Inter-Thin.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 100;
font-display: swap;
src: url("Inter-ThinItalic.woff2?v=3.12") format("woff2"),
url("Inter-ThinItalic.woff?v=3.12") format("woff");
font-family: 'Inter';
font-style: italic;
font-weight: 100;
font-display: swap;
src:
url('Inter-ThinItalic.woff2?v=3.12') format('woff2'),
url('Inter-ThinItalic.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 200;
font-display: swap;
src: url("Inter-ExtraLight.woff2?v=3.12") format("woff2"),
url("Inter-ExtraLight.woff?v=3.12") format("woff");
font-family: 'Inter';
font-style: normal;
font-weight: 200;
font-display: swap;
src:
url('Inter-ExtraLight.woff2?v=3.12') format('woff2'),
url('Inter-ExtraLight.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 200;
font-display: swap;
src: url("Inter-ExtraLightItalic.woff2?v=3.12") format("woff2"),
url("Inter-ExtraLightItalic.woff?v=3.12") format("woff");
font-family: 'Inter';
font-style: italic;
font-weight: 200;
font-display: swap;
src:
url('Inter-ExtraLightItalic.woff2?v=3.12') format('woff2'),
url('Inter-ExtraLightItalic.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url("Inter-Light.woff2?v=3.12") format("woff2"),
url("Inter-Light.woff?v=3.12") format("woff");
font-family: 'Inter';
font-style: normal;
font-weight: 300;
font-display: swap;
src:
url('Inter-Light.woff2?v=3.12') format('woff2'),
url('Inter-Light.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 300;
font-display: swap;
src: url("Inter-LightItalic.woff2?v=3.12") format("woff2"),
url("Inter-LightItalic.woff?v=3.12") format("woff");
font-family: 'Inter';
font-style: italic;
font-weight: 300;
font-display: swap;
src:
url('Inter-LightItalic.woff2?v=3.12') format('woff2'),
url('Inter-LightItalic.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("Inter-Regular.woff2?v=3.12") format("woff2"),
url("Inter-Regular.woff?v=3.12") format("woff");
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src:
url('Inter-Regular.woff2?v=3.12') format('woff2'),
url('Inter-Regular.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url("Inter-Italic.woff2?v=3.12") format("woff2"),
url("Inter-Italic.woff?v=3.12") format("woff");
font-family: 'Inter';
font-style: italic;
font-weight: 400;
font-display: swap;
src:
url('Inter-Italic.woff2?v=3.12') format('woff2'),
url('Inter-Italic.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url("Inter-Medium.woff2?v=3.12") format("woff2"),
url("Inter-Medium.woff?v=3.12") format("woff");
font-family: 'Inter';
font-style: normal;
font-weight: 500;
font-display: swap;
src:
url('Inter-Medium.woff2?v=3.12') format('woff2'),
url('Inter-Medium.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 500;
font-display: swap;
src: url("Inter-MediumItalic.woff2?v=3.12") format("woff2"),
url("Inter-MediumItalic.woff?v=3.12") format("woff");
font-family: 'Inter';
font-style: italic;
font-weight: 500;
font-display: swap;
src:
url('Inter-MediumItalic.woff2?v=3.12') format('woff2'),
url('Inter-MediumItalic.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("Inter-SemiBold.woff2?v=3.12") format("woff2"),
url("Inter-SemiBold.woff?v=3.12") format("woff");
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src:
url('Inter-SemiBold.woff2?v=3.12') format('woff2'),
url('Inter-SemiBold.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 600;
font-display: swap;
src: url("Inter-SemiBoldItalic.woff2?v=3.12") format("woff2"),
url("Inter-SemiBoldItalic.woff?v=3.12") format("woff");
font-family: 'Inter';
font-style: italic;
font-weight: 600;
font-display: swap;
src:
url('Inter-SemiBoldItalic.woff2?v=3.12') format('woff2'),
url('Inter-SemiBoldItalic.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url("Inter-Bold.woff2?v=3.12") format("woff2"),
url("Inter-Bold.woff?v=3.12") format("woff");
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src:
url('Inter-Bold.woff2?v=3.12') format('woff2'),
url('Inter-Bold.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 700;
font-display: swap;
src: url("Inter-BoldItalic.woff2?v=3.12") format("woff2"),
url("Inter-BoldItalic.woff?v=3.12") format("woff");
font-family: 'Inter';
font-style: italic;
font-weight: 700;
font-display: swap;
src:
url('Inter-BoldItalic.woff2?v=3.12') format('woff2'),
url('Inter-BoldItalic.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 800;
font-display: swap;
src: url("Inter-ExtraBold.woff2?v=3.12") format("woff2"),
url("Inter-ExtraBold.woff?v=3.12") format("woff");
font-family: 'Inter';
font-style: normal;
font-weight: 800;
font-display: swap;
src:
url('Inter-ExtraBold.woff2?v=3.12') format('woff2'),
url('Inter-ExtraBold.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 800;
font-display: swap;
src: url("Inter-ExtraBoldItalic.woff2?v=3.12") format("woff2"),
url("Inter-ExtraBoldItalic.woff?v=3.12") format("woff");
font-family: 'Inter';
font-style: italic;
font-weight: 800;
font-display: swap;
src:
url('Inter-ExtraBoldItalic.woff2?v=3.12') format('woff2'),
url('Inter-ExtraBoldItalic.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url("Inter-Black.woff2?v=3.12") format("woff2"),
url("Inter-Black.woff?v=3.12") format("woff");
font-family: 'Inter';
font-style: normal;
font-weight: 900;
font-display: swap;
src:
url('Inter-Black.woff2?v=3.12') format('woff2'),
url('Inter-Black.woff?v=3.12') format('woff');
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 900;
font-display: swap;
src: url("Inter-BlackItalic.woff2?v=3.12") format("woff2"),
url("Inter-BlackItalic.woff?v=3.12") format("woff");
font-family: 'Inter';
font-style: italic;
font-weight: 900;
font-display: swap;
src:
url('Inter-BlackItalic.woff2?v=3.12') format('woff2'),
url('Inter-BlackItalic.woff?v=3.12') format('woff');
}

View file

@ -5,87 +5,87 @@ import { createResource } from 'frappe-ui'
import { userResource } from './user'
export function sessionUser() {
const cookies = new URLSearchParams(document.cookie.split('; ').join('&'))
let _sessionUser = cookies.get('user_id')
if (_sessionUser === 'Guest') {
_sessionUser = null
}
return _sessionUser
const cookies = new URLSearchParams(document.cookie.split('; ').join('&'))
let _sessionUser = cookies.get('user_id')
if (_sessionUser === 'Guest') {
_sessionUser = null
}
return _sessionUser
}
export function sessionSupplierId() {
const cookies = new URLSearchParams(document.cookie.split('; ').join('&'))
return cookies.get('supplier_id')
const cookies = new URLSearchParams(document.cookie.split('; ').join('&'))
return cookies.get('supplier_id')
}
export function sessionSupplierName() {
const cookies = new URLSearchParams(document.cookie.split('; ').join('&'))
return cookies.get('supplier_name')
const cookies = new URLSearchParams(document.cookie.split('; ').join('&'))
return cookies.get('supplier_name')
}
export const session = reactive({
supplier_login: createResource({
url: 'suppliers_portal.suppliers_portal.api.validate_supplier_id',
makeParams({ supplier_id }) {
return {
supplier_id,
}
},
onSuccess(data) {
if (data && data.status === 'success') {
userResource.reload()
session.user = sessionUser()
document.cookie = `supplier_id=${data.supplier_id}`
document.cookie = `supplier_name=${data.supplier_name}`
session.supplier_id = data.supplier_id
session.supplier_name = data.supplier_name
session.login.reset()
router.replace({ name: 'SupplierInvoiceList' })
}
},
onError(error) {
console.error('Login failed', error)
},
}),
login: createResource({
url: 'login',
makeParams({ email, password }) {
return {
usr: email,
pwd: password,
}
},
onSuccess(data) {
if (data) {
userResource.reload()
session.user = sessionUser()
// clear supplier_id and supplier_name
document.cookie = `supplier_id=${session.supplier_id}; expires=Thu, 01 Jan 1970 00:00:00 UTC'`;
document.cookie = `supplier_id=${session.supplier_name}; expires=Thu, 01 Jan 1970 00:00:00 UTC'`;
session.supplier_id = ''
session.supplier_name = ''
session.login.reset()
router.replace({ name: 'SupplierInvoiceList' })
}
},
onError(error) {
console.error('Login failed', error)
},
}),
logout: createResource({
url: 'logout',
onSuccess() {
document.cookie = `supplier_id=${session.supplier_id}; expires=Thu, 01 Jan 1970 00:00:00 UTC'`;
document.cookie = `supplier_id=${session.supplier_name}; expires=Thu, 01 Jan 1970 00:00:00 UTC'`;
session.supplier_id = ''
session.supplier_name = ''
userResource.reset()
session.user = sessionUser()
router.replace({ name: 'Login' })
},
}),
user: sessionUser(),
supplier_id: sessionSupplierId(),
supplier_name: sessionSupplierName(),
isLoggedIn: computed(() => !!session.user),
})
supplier_login: createResource({
url: 'suppliers_portal.suppliers_portal.api.validate_supplier_id',
makeParams({ supplier_id }) {
return {
supplier_id,
}
},
onSuccess(data) {
if (data && data.status === 'success') {
userResource.reload()
session.user = sessionUser()
document.cookie = `supplier_id=${data.supplier_id}`
document.cookie = `supplier_name=${data.supplier_name}`
session.supplier_id = data.supplier_id
session.supplier_name = data.supplier_name
session.login.reset()
router.replace({ name: 'SupplierInvoiceList' })
}
},
onError(error) {
console.error('Login failed', error)
},
}),
login: createResource({
url: 'login',
makeParams({ email, password }) {
return {
usr: email,
pwd: password,
}
},
onSuccess(data) {
if (data) {
userResource.reload()
session.user = sessionUser()
// clear supplier_id and supplier_name
document.cookie = `supplier_id=${session.supplier_id}; expires=Thu, 01 Jan 1970 00:00:00 UTC'`
document.cookie = `supplier_id=${session.supplier_name}; expires=Thu, 01 Jan 1970 00:00:00 UTC'`
session.supplier_id = ''
session.supplier_name = ''
session.login.reset()
router.replace({ name: 'SupplierInvoiceList' })
}
},
onError(error) {
console.error('Login failed', error)
},
}),
logout: createResource({
url: 'logout',
onSuccess() {
document.cookie = `supplier_id=${session.supplier_id}; expires=Thu, 01 Jan 1970 00:00:00 UTC'`
document.cookie = `supplier_id=${session.supplier_name}; expires=Thu, 01 Jan 1970 00:00:00 UTC'`
session.supplier_id = ''
session.supplier_name = ''
userResource.reset()
session.user = sessionUser()
router.replace({ name: 'Login' })
},
}),
user: sessionUser(),
supplier_id: sessionSupplierId(),
supplier_name: sessionSupplierName(),
isLoggedIn: computed(() => !!session.user),
})

View file

@ -2,11 +2,11 @@ import router from '@/router'
import { createResource } from 'frappe-ui'
export const userResource = createResource({
url: 'frappe.auth.get_logged_user',
cache: 'User',
onError(error) {
if (error && error.exc_type === 'AuthenticationError') {
router.push({ name: 'LoginPage' })
}
},
})
url: 'frappe.auth.get_logged_user',
cache: 'User',
onError(error) {
if (error && error.exc_type === 'AuthenticationError') {
router.push({ name: 'LoginPage' })
}
},
})

View file

@ -1,22 +1,22 @@
export const addDays = (date, days) => {
const result = new Date(date);
result.setDate(result.getDate() + days);
return result;
const result = new Date(date)
result.setDate(result.getDate() + days)
return result
}
export const parseInvoiceTerms = (invoiceTerm) => {
switch (invoiceTerm) {
case 'NET 10':
return 10;
case 'NET 15':
return 15;
case 'NET 30':
return 30;
case 'NET 45':
return 45;
case 'NET 60':
return 60;
default:
return 0;
}
}
export const parseInvoiceTerms = invoiceTerm => {
switch (invoiceTerm) {
case 'NET 10':
return 10
case 'NET 15':
return 15
case 'NET 30':
return 30
case 'NET 45':
return 45
case 'NET 60':
return 60
default:
return 0
}
}

View file

@ -7,16 +7,7 @@ import 'bootstrap/dist/css/bootstrap.min.css'
import 'bootstrap'
import 'bootstrap-icons/font/bootstrap-icons.css'
import {
Button,
Card,
Input,
setConfig,
frappeRequest,
resourcesPlugin,
} from 'frappe-ui'
import { Button, Card, Input, setConfig, frappeRequest, resourcesPlugin } from 'frappe-ui'
let app = createApp(App)
@ -29,4 +20,4 @@ app.component('Button', Button)
app.component('Card', Card)
app.component('Input', Input)
app.mount('#app')
app.mount('#app')

View file

@ -4,29 +4,11 @@
<form class="flex flex-col space-y-2 w-full" @submit.prevent="submit">
<Switch v-model="state.forAdmin" label="Login as Admin" />
<div v-if="state.forAdmin">
<Input
required
name="email"
type="text"
placeholder="username@email.com"
label="User ID"
/>
<Input
required
name="password"
type="password"
placeholder="••••••"
label="Password"
/>
<Input required name="email" type="text" placeholder="username@email.com" label="User ID" />
<Input required name="password" type="password" placeholder="••••••" label="Password" />
</div>
<div v-else>
<Input
required
name="supplier_id"
type="text"
placeholder="Supplier ID"
label="Supplier ID"
/>
<Input required name="supplier_id" type="text" placeholder="Supplier ID" label="Supplier ID" />
</div>
<Button :loading="session.login.loading" appearance="primary">Login</Button>
<div v-if="session.login.error" class="text-red-500">

View file

@ -12,13 +12,7 @@
<button type="button" class="btn btn-primary btn-light">
<router-link to="/">Cancel</router-link>
</button>
<button
type="button"
class="btn btn-primary btn-dark"
@click="saveInvoice"
>
Save
</button>
<button type="button" class="btn btn-primary btn-dark" @click="saveInvoice">Save</button>
</div>
</div>
</div>
@ -29,24 +23,19 @@
<div class="mb-2 row">
<div class="col">
<div class="row">
<label for="supplier_invoice_number" class="col-sm-3 col-form-label"
>Invoice Number</label
>
<label for="bill_no" class="col-sm-3 col-form-label">Invoice Number</label>
<div class="col-sm-8">
<input
type="text"
class="form-control"
id="supplier_invoice_number"
v-model.number="invoiceData.supplier_invoice_number"
/>
id="bill_no"
v-model.number="invoiceData.bill_no" />
</div>
</div>
</div>
<div class="col">
<div class="row">
<label for="service_date" class="col-sm-3 col-form-label"
>Service Date</label
>
<label for="service_date" class="col-sm-3 col-form-label">Service Date</label>
<div class="col-sm-8">
<TextInput
:type="'date'"
@ -55,8 +44,7 @@
placeholder="Service Date"
id="service_date"
:disabled="false"
v-model="invoiceData.service_date"
/>
v-model="invoiceData.service_date" />
</div>
</div>
</div>
@ -65,35 +53,29 @@
<div class="mb-2 row">
<div class="col">
<div class="row">
<label for="supplier_id" class="col-sm-3 col-form-label"
>Supplier ID</label
>
<label for="supplier_id" class="col-sm-3 col-form-label">Supplier ID</label>
<div class="col-sm-8">
<input
type="text"
readonly
class="form-control-plaintext"
id="supplier_id"
v-model="invoiceData.supplier"
/>
v-model="invoiceData.supplier" />
</div>
</div>
</div>
<div class="col">
<div class="row">
<label for="invoice_date" class="col-sm-3 col-form-label"
>Invoice Date</label
>
<label for="posting_date" class="col-sm-3 col-form-label">Invoice Date</label>
<div class="col-sm-8">
<TextInput
:type="'date'"
size="lg"
variant="outline"
placeholder="Invoice Date"
id="invoice_date"
id="posting_date"
:disabled="false"
v-model="invoiceData.invoice_date"
/>
v-model="invoiceData.posting_date" />
</div>
</div>
</div>
@ -102,9 +84,7 @@
<div class="mb-2 row">
<div class="col">
<div class="row">
<label for="supplier_name" class="col-sm-3 col-form-label"
>Supplier Name</label
>
<label for="supplier_name" class="col-sm-3 col-form-label">Supplier Name</label>
<div class="col-sm-8">
<input
type="text"
@ -112,36 +92,28 @@
class="form-control-plaintext"
id="supplier_name"
:value="invoiceData.supplier_name"
v-if="!isAdministrator"
/>
v-if="!isAdministrator" />
<Autocomplete
id="supplier_name"
:options="portalSuppliers.data"
:placeholder="'Supplier Name'"
:hideSearch="!invoiceData.supplier_invoice_number"
:hideSearch="!invoiceData.bill_no"
:label="'company_name'"
v-model="invoiceData.supplier_name"
v-else
/>
v-else />
</div>
</div>
</div>
<div class="col">
<div class="row">
<label for="invoice_terms" class="col-sm-3 col-form-label"
>Invoice Terms</label
>
<label for="payment_terms_template" class="col-sm-3 col-form-label">Invoice Terms</label>
<div class="col-sm-8">
<select
class="form-select form-select-mb h-100 d-inline-block"
aria-label="Invoice Terms"
v-model="invoiceData.invoice_terms"
>
v-model="invoiceData.payment_terms_template">
<option readonly></option>
<option
v-for="option in invoiceTermOptions"
:value="option.value"
>
<option v-for="option in invoiceTermOptions" :value="option.value">
{{ option.label }}
</option>
</select>
@ -153,9 +125,7 @@
<div class="col"></div>
<div class="col">
<div class="row">
<label for="invoice_date" class="col-sm-3 col-form-label"
>Due Date</label
>
<label for="posting_date" class="col-sm-3 col-form-label">Due Date</label>
<div class="col-sm-8">
<TextInput
:type="'date'"
@ -164,8 +134,7 @@
placeholder="Due Date"
id="due_date"
:disabled="true"
v-model="invoiceDueDate"
/>
v-model="invoiceDueDate" />
</div>
</div>
</div>
@ -182,80 +151,43 @@
data-bs-toggle="collapse"
data-bs-target="#collapseOne"
aria-expanded="true"
aria-controls="collapseOne"
>
aria-controls="collapseOne">
Service Address
</button>
</h2>
<div
id="collapseOne"
class="accordion-collapse collapse show"
data-bs-parent="#service-address-accordion"
>
data-bs-parent="#service-address-accordion">
<div class="accordion-body">
<div class="row mb-1">
<label for="site_code" class="col-sm-3 col-form-label"
>Site Code</label
>
<label for="site_code" class="col-sm-3 col-form-label">Site Code</label>
<div class="col-sm-8">
<input
type="text"
class="form-control"
id="site_code"
v-model="invoiceData.site_code"
/>
<input type="text" class="form-control" id="site_code" v-model="invoiceData.site_code" />
</div>
</div>
<div class="row mb-1">
<label for="street" class="col-sm-3 col-form-label"
>Street</label
>
<label for="street" class="col-sm-3 col-form-label">Street</label>
<div class="col-sm-8">
<input
type="text"
class="form-control"
id="street"
v-model="invoiceData.street"
/>
<input type="text" class="form-control" id="street" v-model="invoiceData.street" />
</div>
</div>
<div class="row mb-1">
<label for="city" class="col-sm-3 col-form-label"
>City</label
>
<label for="city" class="col-sm-3 col-form-label">City</label>
<div class="col-sm-8">
<input
type="text"
class="form-control"
id="city"
v-model="invoiceData.city"
/>
<input type="text" class="form-control" id="city" v-model="invoiceData.city" />
</div>
</div>
<div class="row mb-1">
<label for="state" class="col-sm-3 col-form-label"
>State</label
>
<label for="state" class="col-sm-3 col-form-label">State</label>
<div class="col-sm-8">
<input
type="text"
class="form-control"
id="state"
v-model="invoiceData.state"
/>
<input type="text" class="form-control" id="state" v-model="invoiceData.state" />
</div>
</div>
<div class="row mb-1">
<label for="zip_code" class="col-sm-3 col-form-label"
>Zip Code</label
>
<label for="zip_code" class="col-sm-3 col-form-label">Zip Code</label>
<div class="col-sm-8">
<input
type="text"
class="form-control"
id="zip_code"
v-model="invoiceData.zip_code"
/>
<input type="text" class="form-control" id="zip_code" v-model="invoiceData.zip_code" />
</div>
</div>
</div>
@ -273,30 +205,22 @@
data-bs-toggle="collapse"
data-bs-target="#collapse-2"
aria-expanded="true"
aria-controls="collapse-2"
>
aria-controls="collapse-2">
Notes
</button>
</h2>
<div
id="collapse-2"
class="accordion-collapse collapse show"
data-bs-parent="#address-accordion"
>
<div id="collapse-2" class="accordion-collapse collapse show" data-bs-parent="#address-accordion">
<div class="accordion-body">
<div class="row mb-1">
<label for="notes" class="col-sm-3 col-form-label"
>Notes</label
>
<label for="remarks" class="col-sm-3 col-form-label">Notes</label>
<div class="col-md-8">
<Textarea
:variant="'subtle'"
size="md"
placeholder="Enter Notes Here"
:disabled="false"
v-model="invoiceData.notes"
id="notes"
/>
v-model="invoiceData.remarks"
id="remarks" />
</div>
</div>
</div>
@ -327,44 +251,27 @@
<select
class="form-select form-select-mb h-100 d-inline-block"
aria-label="Service Types"
v-model="item.service_type"
>
v-model="item.item_code">
<option readonly></option>
<option
v-for="option in serviceTypeOption"
:value="option.value"
>
<option v-for="option in serviceTypeOption" :value="option.value">
{{ option.label }}
</option>
</select>
</div>
</td>
<td>
<input type="text" class="form-control" v-model="item.quantity" />
<input type="text" class="form-control" v-model="item.qty" />
</td>
<td>
<div class="input-group">
<span class="input-group-text"
><i class="bi bi-currency-dollar"></i>
</span>
<input
type="text"
class="form-control"
v-model.number="item.rate"
/>
<span class="input-group-text"><i class="bi bi-currency-dollar"></i> </span>
<input type="text" class="form-control" v-model.number="item.rate" />
</div>
</td>
<td>
<div class="input-group">
<span class="input-group-text"
><i class="bi bi-currency-dollar"></i>
</span>
<input
type="text"
readonly
class="form-control"
v-model.number="item.amount"
/>
<span class="input-group-text"><i class="bi bi-currency-dollar"></i> </span>
<input type="text" readonly class="form-control" v-model.number="item.amount" />
</div>
</td>
<td>
@ -390,20 +297,11 @@
<div class="col"></div>
<div class="col">
<div class="input-group">
<label for="total_amount" class="col-sm-3 col-form-label"
>Total Amount</label
>
<label for="grand_total" class="col-sm-3 col-form-label">Total Amount</label>
<div class="cols-sm-5">
<div class="input-group">
<span class="input-group-text currency-fmt"
><i class="bi bi-currency-dollar"></i>
</span>
<input
type="text"
readonly
class="form-control"
v-model.number="invoiceData.total_amount"
/>
<span class="input-group-text currency-fmt"><i class="bi bi-currency-dollar"></i> </span>
<input type="text" readonly class="form-control" v-model.number="invoiceData.grand_total" />
</div>
</div>
</div>
@ -427,7 +325,7 @@ import Autocomplete from 'frappe-ui/src/components/Autocomplete.vue'
const isAdministrator = sessionUser() === 'Administrator'
const portalInvoice = createListResource({
doctype: 'Invoices',
doctype: 'Purchase Invoice',
insert: {
onSuccess() {
router.push({ name: 'SupplierInvoiceList' })
@ -435,6 +333,13 @@ const portalInvoice = createListResource({
},
})
const portalSuppliers = createListResource({
doctype: 'Supplier',
fields: ['name as value', 'supplier_name as label'],
orderBy: 'creation desc',
})
portalSuppliers.fetch()
const invoiceTermOptions = [
{
label: 'NET 10',
@ -474,28 +379,25 @@ const serviceTypeOption = [
]
const invoiceData = reactive({
supplier_invoice_number: '',
bill_no: '',
supplier: sessionSupplierId(),
service_date: new Date().toISOString().split('T')[0],
invoice_date: new Date().toISOString().split('T')[0],
posting_date: new Date().toISOString().split('T')[0],
supplier_name: sessionSupplierName(),
invoice_terms: '',
total_amount: 0,
payment_terms_template: '',
grand_total: 0,
site_code: '',
street: '',
city: '',
state: '',
zip_code: '',
notes: '',
remarks: '',
})
const invoiceItems = reactive([])
let invoiceDueDate = computed(() => {
if (invoiceData.invoice_terms) {
let result = addDays(
invoiceData.invoice_date,
parseInvoiceTerms(invoiceData.invoice_terms),
)
if (invoiceData.payment_terms_template) {
let result = addDays(invoiceData.posting_date, parseInvoiceTerms(invoiceData.payment_terms_template))
// return as string
return result.toISOString().split('T')[0]
}
@ -504,61 +406,52 @@ let invoiceDueDate = computed(() => {
const addRow = () => {
const rowObject = reactive({
service_type: '',
quantity: 0,
item_code: '',
qty: 0,
rate: 0,
amount: 0,
})
rowObject.amount = computed(() => rowObject.quantity * rowObject.rate)
rowObject.amount = computed(() => rowObject.qty * rowObject.rate)
invoiceItems.push(rowObject)
}
const deleteRow = (index) => {
const deleteRow = index => {
invoiceItems.splice(index, 1)
}
invoiceData.total_amount = computed(() => {
invoiceData.grand_total = computed(() => {
return invoiceItems.reduce((acc, item) => acc + item.amount, 0)
})
const saveInvoice = () => {
portalInvoice.insert.submit({
supplier_invoice_number: invoiceData.supplier_invoice_number,
supplier: invoiceData.supplier,
service_date: invoiceData.service_date,
invoice_date: invoiceData.invoice_date,
supplier_name: invoiceData.supplier_name,
invoice_terms: invoiceData.invoice_terms,
...invoiceData,
set_posting_time: true,
due_date: invoiceDueDate.value,
site_code: invoiceData.site_code,
street: invoiceData.street,
city: invoiceData.city,
state: invoiceData.state,
zip_code: invoiceData.zip_code,
notes: invoiceData.notes,
supplier_invoice_items: invoiceItems.map((item) => {
remarks: invoiceData.remarks,
items: invoiceItems.map(item => {
return {
service_type: item.service_type,
quantity: item.quantity,
item_code: item.item_code,
qty: item.qty,
rate: item.rate,
}
}),
})
}
const portalSuppliers = createListResource({
doctype: 'Supplier',
fields: ['name as value', 'company_name as label'],
orderBy: 'creation desc',
})
portalSuppliers.fetch()
// watch for changes in invoiceData.supplier_name
watch(
() => invoiceData.supplier_name,
(newVal) => {
newVal => {
if (newVal) {
invoiceData.supplier = newVal.value
}
},
}
)
</script>

View file

@ -1,12 +1,12 @@
<template>
<div v-if="!invoice.loading && invoice.doc">
<div v-if="!invoice.loading && documents.invoice">
<div class="container mt-5 py-3 border">
<div class="row mb-3 vr-aligned">
<div class="col-6">
<h1 class="text-gray-900 font-bold text-[32px]">
Supplier Invoice: {{ invoice.doc.name }}
Supplier Invoice: {{ documents.invoice.name }}
<Badge>
{{ invoice.doc.status }}
{{ documents.invoice.status }}
</Badge>
</h1>
</div>
@ -22,35 +22,24 @@
</div>
</div>
</div>
<div
:class="
invoice.doc.supplier_invoice_items
? 'container border-bottom pb-3'
: 'container pb-3'
"
>
<div :class="documents.invoice.items ? 'container border-bottom pb-3' : 'container pb-3'">
<div class="mb-1 row">
<div class="col">
<div class="row">
<label for="suppplier_id" class="col-sm-3 col-form-label"
>Supplier ID</label
>
<label for="suppplier_id" class="col-sm-3 col-form-label">Supplier ID</label>
<div class="col-sm-8">
<input
type="text"
readonly
class="form-control-plaintext"
id="suppplier_id"
:value="invoice.doc.supplier"
/>
:value="documents.invoice.supplier" />
</div>
</div>
</div>
<div class="col">
<div class="row mb-1">
<label for="service_date" class="col-sm-3 col-form-label"
>Service Date</label
>
<label for="service_date" class="col-sm-3 col-form-label">Service Date</label>
<div class="col-sm-8">
<TextInput
:type="'date'"
@ -59,8 +48,7 @@
placeholder="Payment Date"
id="service_date"
:disabled="true"
v-model="invoice.doc.service_date"
/>
v-model="documents.invoice.service_date" />
</div>
</div>
</div>
@ -68,35 +56,29 @@
<div class="mb-1 row">
<div class="col">
<div class="row">
<label for="suppplier_name" class="col-sm-3 col-form-label"
>Supplier Name</label
>
<label for="suppplier_name" class="col-sm-3 col-form-label">Supplier Name</label>
<div class="col-sm-8">
<input
type="text"
readonly
class="form-control-plaintext"
id="suppplier_name"
:value="invoice.doc.supplier_name"
/>
:value="documents.invoice.supplier_name" />
</div>
</div>
</div>
<div class="col">
<div class="row mb-1">
<label for="invoice_date" class="col-sm-3 col-form-label"
>Invoice Date</label
>
<label for="posting_date" class="col-sm-3 col-form-label">Invoice Date</label>
<div class="col-sm-8">
<TextInput
:type="'date'"
size="lg"
variant="outline"
placeholder="Invoice Date"
id="invoice_date"
id="posting_date"
:disabled="true"
v-model="invoice.doc.invoice_date"
/>
v-model="documents.invoice.posting_date" />
</div>
</div>
</div>
@ -114,8 +96,7 @@
placeholder="Due Date"
id="due_date"
:disabled="true"
v-model="invoice.doc.due_date"
/>
v-model="documents.invoice.due_date" />
</div>
</div>
</div>
@ -124,17 +105,14 @@
<div class="col"></div>
<div class="col">
<div class="row">
<label for="invoice_terms" class="col-sm-3 col-form-label"
>Invoice Terms</label
>
<label for="payment_terms_template" class="col-sm-3 col-form-label">Invoice Terms</label>
<div class="col-sm-8">
<input
type="text"
readonly
class="form-control-plaintext h-100 d-inline-block"
id="invoice_terms"
:value="invoice.doc.invoice_terms"
/>
id="payment_terms_template"
:value="documents.invoice.payment_terms_template" />
</div>
</div>
</div>
@ -150,85 +128,65 @@
data-bs-toggle="collapse"
data-bs-target="#collapseOne"
aria-expanded="true"
aria-controls="collapseOne"
>
aria-controls="collapseOne">
Service Address
</button>
</h2>
<div
id="collapseOne"
class="accordion-collapse collapse"
data-bs-parent="#service-address-accordion"
>
<div id="collapseOne" class="accordion-collapse collapse" data-bs-parent="#service-address-accordion">
<div class="accordion-body">
<div class="row mb-1">
<label for="site_code" class="col-sm-3 col-form-label"
>Site Code</label
>
<label for="site_code" class="col-sm-3 col-form-label">Site Code</label>
<div class="col-sm-8">
<input
type="text"
readonly
class="form-control-plaintext"
id="site_code"
:value="invoice.doc.site_code"
/>
:value="documents?.address?.doc?.site_code" />
</div>
</div>
<div class="row mb-1">
<label for="street" class="col-sm-3 col-form-label"
>Street</label
>
<label for="street" class="col-sm-3 col-form-label">Street</label>
<div class="col-sm-8">
<input
type="text"
readonly
class="form-control-plaintext"
id="street"
:value="invoice.doc.street"
/>
:value="documents?.address?.doc?.address_line1" />
</div>
</div>
<div class="row mb-1">
<label for="city" class="col-sm-3 col-form-label"
>City</label
>
<label for="city" class="col-sm-3 col-form-label">City</label>
<div class="col-sm-8">
<input
type="text"
readonly
class="form-control-plaintext"
id="city"
:value="invoice.doc.city"
/>
:value="documents?.address?.doc?.city" />
</div>
</div>
<div class="row mb-1">
<label for="state" class="col-sm-3 col-form-label"
>State</label
>
<label for="state" class="col-sm-3 col-form-label">State</label>
<div class="col-sm-8">
<input
type="text"
readonly
class="form-control-plaintext"
id="state"
:value="invoice.doc.state"
/>
:value="documents?.address?.doc?.state" />
</div>
</div>
<div class="row mb-1">
<label for="zip_code" class="col-sm-3 col-form-label"
>Zip Code</label
>
<label for="zip_code" class="col-sm-3 col-form-label">Zip Code</label>
<div class="col-sm-8">
<input
type="text"
readonly
class="form-control-plaintext"
id="zip_code"
:value="invoice.doc.zip_code"
/>
:value="documents?.address?.doc?.pincode" />
</div>
</div>
</div>
@ -246,31 +204,23 @@
data-bs-toggle="collapse"
data-bs-target="#collapse-2"
aria-expanded="true"
aria-controls="collapse-2"
>
aria-controls="collapse-2">
Notes
</button>
</h2>
<div
id="collapse-2"
class="accordion-collapse collapse"
data-bs-parent="#address-accordion"
>
<div id="collapse-2" class="accordion-collapse collapse" data-bs-parent="#address-accordion">
<div class="accordion-body">
<div class="row mb-1">
<label for="notes" class="col-sm-4 col-form-label"
>Notes</label
>
<label for="remarks" class="col-sm-4 col-form-label">Notes</label>
<div class="col-sm-8">
<Textarea
:variant="'subtle'"
size="md"
placeholder="Enter Notes Here"
:disabled="true"
v-model="invoice.doc.notes"
v-model="documents.invoice.remarks"
label=""
id="notes"
/>
id="remarks" />
</div>
</div>
</div>
@ -281,7 +231,7 @@
</div>
</div>
<div class="container" v-if="invoice.doc.supplier_invoice_items">
<div class="container" v-if="documents.invoice.items">
<table class="table">
<thead>
<tr>
@ -293,13 +243,10 @@
</tr>
</thead>
<tbody class="table-group-divider">
<tr
v-for="(item, index) in invoice.doc.supplier_invoice_items"
:key="item.name"
>
<tr v-for="(item, index) in documents.invoice.items" :key="item.name">
<th scope="row">{{ index + 1 }}</th>
<td>{{ item.service_type }}</td>
<td>{{ item.quantity }}</td>
<td>{{ item.item_code }}</td>
<td>{{ item.qty }}</td>
<td><i class="bi bi-currency-dollar"></i> {{ item.rate }}</td>
<td><i class="bi bi-currency-dollar"></i> {{ item.amount }}</td>
</tr>
@ -312,20 +259,11 @@
<div class="col"></div>
<div class="col">
<div class="input-group">
<label for="total_amount" class="col-sm-3 col-form-label"
>Total Amount</label
>
<label for="grand_total" class="col-sm-3 col-form-label">Total Amount</label>
<div class="cols-sm-5">
<div class="input-group">
<span class="input-group-text"
><i class="bi bi-currency-dollar"></i>
</span>
<input
type="text"
readonly
class="form-control"
v-model.number="invoice.doc.total_amount"
/>
<span class="input-group-text"><i class="bi bi-currency-dollar"></i> </span>
<input type="text" readonly class="form-control" v-model.number="documents.invoice.grand_total" />
</div>
</div>
</div>
@ -336,22 +274,28 @@
</div>
</template>
<script setup>
<script setup lang="ts">
import { createDocumentResource, createListResource, Badge } from 'frappe-ui'
import Textarea from 'frappe-ui/src/components/Textarea.vue'
import TextInput from 'frappe-ui/src/components/TextInput.vue'
import { reactive, watch } from 'vue'
const props = defineProps({
supplierInvoiceNumber: {
type: String,
required: true,
},
})
const { supplierInvoiceNumber } = defineProps<{
supplierInvoiceNumber: string
}>()
const documents = reactive({})
const invoice = createDocumentResource({
doctype: 'Invoices',
name: props.supplierInvoiceNumber,
auto: true,
doctype: 'Purchase Invoice',
name: supplierInvoiceNumber,
onSuccess: (data) => {
documents.invoice = data
documents.address = createDocumentResource({
doctype: 'Address',
name: data.supplier_address,
auto: true
})
}
})
</script>

View file

@ -16,13 +16,7 @@
<router-link to="/invoices/new">
<button type="button" class="btn btn-primary btn-dark">New</button>
</router-link>
<button
type="button"
class="btn btn-primary btn-dark"
@click="session.logout.submit"
>
Logout
</button>
<button type="button" class="btn btn-primary btn-dark" @click="session.logout.submit">Logout</button>
</div>
</div>
</div>
@ -52,7 +46,7 @@
<td>
<Badge>{{ item.status }}</Badge>
</td>
<td><i class="bi bi-currency-dollar"></i> {{ item.total_amount }}</td>
<td><i class="bi bi-currency-dollar"></i> {{ item.grand_total }}</td>
</tr>
</tbody>
</table>
@ -76,7 +70,7 @@ onBeforeMount(() => {
})
let invoiceParams = {
doctype: 'Invoices',
doctype: 'Purchase Invoice',
fields: ['*'],
orderBy: 'creation desc',
start: 0,

View file

@ -3,49 +3,49 @@ import { session } from './data/session'
import { userResource } from '@/data/user'
const routes = [
{
name: 'Login',
path: '/login',
component: () => import('@/pages/Login.vue'),
},
{
name: 'SupplierInvoiceList',
path: '/',
component: () => import('@/pages/SupplierInvoiceList.vue'),
},
{
name: 'SupplierInvoice',
path: '/invoices/:supplierInvoiceNumber',
component: () => import('@/pages/SupplierInvoice.vue'),
props: true,
},
{
name: 'SupplierInvoiceCreate',
path: '/invoices/new',
component: () => import('@/pages/NewInvoice.vue'),
},
{
name: 'Login',
path: '/login',
component: () => import('@/pages/Login.vue'),
},
{
name: 'SupplierInvoiceList',
path: '/',
component: () => import('@/pages/SupplierInvoiceList.vue'),
},
{
name: 'SupplierInvoice',
path: '/invoices/:supplierInvoiceNumber',
component: () => import('@/pages/SupplierInvoice.vue'),
props: true,
},
{
name: 'SupplierInvoiceCreate',
path: '/invoices/new',
component: () => import('@/pages/NewInvoice.vue'),
},
]
let router = createRouter({
history: createWebHistory('/supplier-portal'),
routes,
history: createWebHistory('/supplier-portal'),
routes,
})
router.beforeEach(async (to, from, next) => {
let isLoggedIn = session.isLoggedIn
try {
await userResource.promise
} catch (error) {
isLoggedIn = false
}
let isLoggedIn = session.isLoggedIn
try {
await userResource.promise
} catch (error) {
isLoggedIn = false
}
if (to.name === 'Login' && isLoggedIn) {
next({ name: 'SupplierInvoiceList' })
} else if (to.name !== 'Login' && !isLoggedIn) {
next({ name: 'Login' })
} else {
next()
}
if (to.name === 'Login' && isLoggedIn) {
next({ name: 'SupplierInvoiceList' })
} else if (to.name !== 'Login' && !isLoggedIn) {
next({ name: 'Login' })
} else {
next()
}
})
export default router
export default router

View file

@ -1,18 +1,16 @@
module.exports = {
presets: [
require('frappe-ui/src/utils/tailwind.config')
],
content: [
"./index.html",
"./src/**/*.{vue,js,ts,jsx,tsx}",
"./node_modules/frappe-ui/src/components/**/*.{vue,js,ts,jsx,tsx}",
],
theme: {
extend: {
container: {
center: true,
}
},
},
plugins: [],
presets: [require('frappe-ui/src/utils/tailwind.config')],
content: [
'./index.html',
'./src/**/*.{vue,js,ts,jsx,tsx}',
'./node_modules/frappe-ui/src/components/**/*.{vue,js,ts,jsx,tsx}',
],
theme: {
extend: {
container: {
center: true,
},
},
},
plugins: [],
}

View file

@ -5,18 +5,18 @@ import frappeui from 'frappe-ui/vite'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [frappeui(), vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
build: {
outDir: `../${path.basename(path.resolve('..'))}/public/supplier-portal`,
emptyOutDir: true,
target: 'es2015',
},
optimizeDeps: {
include: ['frappe-ui > feather-icons', 'showdown', 'engine.io-client'],
},
})
plugins: [frappeui(), vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
},
},
build: {
outDir: `../${path.basename(path.resolve('..'))}/public/supplier-portal`,
emptyOutDir: true,
target: 'es2015',
},
optimizeDeps: {
include: ['frappe-ui > feather-icons', 'showdown', 'engine.io-client'],
},
})

View file

@ -137,31 +137,32 @@ app_license = "mit"
# ---------------
# Hook on document methods and events
# doc_events = {
# "*": {
# "on_update": "method",
# "on_cancel": "method",
# "on_trash": "method"
# }
# }
doc_events = {
"Supplier": {
"autoname": "suppliers_portal.overrides.supplier.generate_supplier_id",
"after_insert": "suppliers_portal.overrides.supplier.create_user",
}
}
# Scheduled Tasks
# ---------------
scheduler_events = {
# "all": [
# "suppliers_portal.tasks.all"
# ],
"daily": ["suppliers_portal.utils.daily_invoice_status_update"],
# "hourly": [
# "suppliers_portal.tasks.hourly"
# ],
# "weekly": [
# "suppliers_portal.tasks.weekly"
# ],
# "monthly": [
# "suppliers_portal.tasks.monthly"
# ],
# "all": [
# "suppliers_portal.tasks.all"
# ],
# "daily": [
# "suppliers_portal.tasks.daily"
# ],
# "hourly": [
# "suppliers_portal.tasks.hourly"
# ],
# "weekly": [
# "suppliers_portal.tasks.weekly"
# ],
# "monthly": [
# "suppliers_portal.tasks.monthly"
# ],
}
# Testing
@ -242,6 +243,6 @@ scheduler_events = {
website_route_rules = [
{"from_route": "/supplier-portal/<path:app_path>", "to_route": "supplier-portal"},
{"from_route": "/erpnext", "to_route": "/app"},
{"from_route": "/supplier-portal/<path:app_path>", "to_route": "supplier-portal"},
{"from_route": "/erpnext", "to_route": "/app"},
]

View file

@ -0,0 +1,47 @@
import random
import frappe
def generate_supplier_id(supplier, method=None):
# skip ID creation during patch migrations
if frappe.flags.in_patch:
return
length = 8
supplier_id = random.randint(10 ** (length - 1), 10**length - 1)
while frappe.db.exists("Supplier", {"supplier_id": supplier_id}):
supplier_id = random.randint(10 ** (length - 1), 10**length - 1)
supplier.name = str(supplier_id)
def create_user(supplier, method=None):
# skip user creation during patch migrations
if frappe.flags.in_patch:
return
# create role if it doesn't exist
if not frappe.db.exists("Role", "Portal Supplier"):
frappe.get_doc(
{
"doctype": "Role",
"role_name": "Portal Supplier",
"desk_access": False,
}
).insert()
user = frappe.new_doc("User")
user.update(
{
"email": f"{supplier.name}@supplier-portal.com",
"first_name": supplier.supplier_name,
"roles": [{"role": "Portal Supplier"}],
"new_password": supplier.name,
"send_welcome_email": False,
}
)
user.flags.ignore_password_policy = True
user.insert()
supplier.append("portal_users", {"user": user.name})
supplier.save()

View file

@ -3,4 +3,5 @@
# Read docs to understand patches: https://frappeframework.com/docs/v14/user/en/database-migrations
[post_model_sync]
# Patches added in this section will be executed after doctypes are migrated
# Patches added in this section will be executed after doctypes are migrated
suppliers_portal.patches.move_invoices_to_erpnext

View file

@ -0,0 +1,346 @@
import json
import frappe
from erpnext.accounts.doctype.account.account import update_account_number
from erpnext.accounts.utils import get_account_name
from erpnext.setup.utils import enable_all_roles_and_domains
from frappe.contacts.doctype.address.address import get_address_display
from frappe.desk.page.setup_wizard.setup_wizard import setup_complete
from frappe.utils import getdate, update_progress_bar
######################## PATCH PREREQUISITES ########################
# - This patch should be run on a fresh site with ERPNext installed (`suppliers_portal` should be installed after `erpnext`)
# - The following files (containing exported doctype records) must be setup in the
# site's private backups folder:
# - users.json (the 'send_welcome_email' field should be set to 0 otherwise it'll
# try to send welcome emails)
# - suppliers.json (remove duplicate entry for 'Superior Backflow Services, LLC')
# - invoices.json
def execute():
setup_site()
setup_accounts()
create_fiscal_years()
create_roles()
import_users()
create_items()
create_payment_terms_template()
import_suppliers()
import_invoices()
def setup_site():
print("Setting up site")
frappe.clear_cache()
today = getdate()
setup_complete(
{
"currency": "USD",
"full_name": "Administrator",
"company_name": "Sea Haven Industries",
"timezone": "America/New_York",
"company_abbr": "SHI",
"domains": ["Services"],
"country": "United States",
"fy_start_date": today.replace(month=1, day=1).isoformat(),
"fy_end_date": today.replace(month=12, day=31).isoformat(),
"language": "en-US",
"company_tagline": "Sea Haven Industries",
"email": "support@seahavenindustries.com",
"password": "admin",
"chart_of_accounts": "Standard with Numbers",
"bank_account": "Primary Checking",
}
)
enable_all_roles_and_domains()
for module in frappe.get_all("Module Onboarding"):
frappe.db.set_value("Module Onboarding", module, "is_complete", True)
frappe.db.set_value("User", "Administrator", "time_zone", "America/New_York")
frappe.db.commit()
def setup_accounts():
abbr = frappe.db.get_value("Company", "Sea Haven Industries", "abbr")
frappe.rename_doc(
"Account",
f"1000 - Application of Funds (Assets) - {abbr}",
f"1000 - Assets - {abbr}",
force=True,
)
frappe.rename_doc(
"Account",
f"2000 - Source of Funds (Liabilities) - {abbr}",
f"2000 - Liabilities - {abbr}",
force=True,
)
frappe.rename_doc(
"Account",
f"1310 - Debtors - {abbr}",
f"1310 - Accounts Receivable - {abbr}",
force=True,
)
frappe.rename_doc(
"Account",
f"2110 - Creditors - {abbr}",
f"2110 - Accounts Payable - {abbr}",
force=True,
)
update_account_number(f"1110 - Cash - {abbr}", "Petty Cash", account_number="1110")
update_account_number(
f"Primary Checking - {abbr}", "Primary Checking", account_number="1201"
)
def create_fiscal_years():
for year in range(2020, 2027):
if not frappe.db.exists("Fiscal Year", str(year)):
fiscal_year = frappe.new_doc("Fiscal Year")
fiscal_year.update(
{
"year": str(year),
"year_start_date": getdate(f"{year}-01-01"),
"year_end_date": getdate(f"{year}-12-31"),
}
)
fiscal_year.insert(ignore_permissions=True)
def create_roles():
roles = ["Portal Supplier", "Supplier"]
for role in roles:
if not frappe.db.exists("Role", role):
role_doc = frappe.new_doc("Role")
role_doc.update(
{
"role_name": role,
"desk_access": False,
}
)
role_doc.insert(ignore_permissions=True)
def import_users():
print("Importing users")
users_file = frappe.get_site_path("private", "backups", "users.json")
frappe.import_doc(users_file)
def create_items():
for item in ["Preventive Maintence", "Repair"]:
if not frappe.db.exists("Item", item):
item_doc = frappe.new_doc("Item")
item_doc.update(
{
"item_code": item,
"item_name": item,
"item_group": "Services",
"stock_uom": "Unit",
}
)
item_doc.insert(ignore_permissions=True)
def create_payment_terms_template():
for credit_days in [0, 10, 15, 30, 45, 60]:
template_name = f"NET {credit_days}" if credit_days else "Due On Receipt"
if not frappe.db.exists("Payment Terms Template", template_name):
term = frappe.new_doc("Payment Term")
term.update(
{
"payment_term_name": template_name,
"invoice_portion": 100,
"due_date_based_on": "Day(s) after invoice date",
"credit_days": credit_days,
}
)
term.save(ignore_permissions=True)
template = frappe.new_doc("Payment Terms Template")
template.template_name = template_name
template.append(
"terms",
{
"payment_term": term.name,
"invoice_portion": term.invoice_portion,
"due_date_based_on": term.due_date_based_on,
"credit_days": term.credit_days,
},
)
template.save(ignore_permissions=True)
def create_address(doc):
address = frappe.new_doc("Address")
address.update(
{
"address_type": "Billing",
"address_title": doc.get("title"),
"address_line1": doc.get("street"),
"city": doc.get("city") or "Unknown",
"state": doc.get("state"),
"pincode": doc.get("zip_code"),
"country": "United States",
"is_primary_address": True,
"is_shipping_address": True,
"links": [
{
"link_doctype": doc.get("ref_doctype"),
"link_name": doc.get("ref_name"),
},
],
}
)
try:
address.insert(ignore_permissions=True)
except Exception as e:
print(f"Error inserting address for {doc.get('ref_name')}: {e}")
return frappe._dict()
return address
def import_suppliers():
suppliers_file = frappe.get_site_path("private", "backups", "suppliers.json")
with open(suppliers_file) as f:
suppliers = json.load(f)
for idx, supplier in enumerate(suppliers):
update_progress_bar("Importing suppliers", idx, (len(suppliers)))
# skip if supplier already exists
if frappe.db.exists("Supplier", {"name": supplier.get("name")}):
continue
supplier_doc = frappe.new_doc("Supplier")
supplier_doc.update(
{
"supplier_name": supplier.get("company_name"),
"portal_users": [{"user": supplier.get("user")}],
"owner": supplier.get("owner"),
"creation": supplier.get("creation"),
}
)
supplier_doc.insert(ignore_permissions=True)
supplier_docname = frappe.rename_doc(
"Supplier", supplier_doc.name, supplier.get("name"), force=True
)
if supplier.get("street"):
address = create_address(
{
**supplier,
"title": supplier.get("company_name"),
"ref_doctype": "Supplier",
"ref_name": supplier_docname,
}
)
if address:
address_display = get_address_display(address.name)
supplier_doc.db_set("supplier_primary_address", address.name)
supplier_doc.db_set("primary_address", address_display)
def import_invoices():
invoices_file = frappe.get_site_path("private", "backups", "invoices.json")
with open(invoices_file) as f:
invoices = json.load(f)
for idx, invoice in enumerate(invoices):
update_progress_bar("Importing invoices", idx, (len(invoices)))
# skip if purchase invoice already exists
if frappe.db.exists("Purchase Invoice", {"name": invoice.get("name")}):
continue
invoice_doc = frappe.new_doc("Purchase Invoice")
invoice_doc.update(
{
"supplier": invoice.get("supplier"),
"bill_no": invoice.get("supplier_invoice_number"),
"site_code": invoice.get("site_code"),
"set_posting_time": True,
"bill_date": getdate(invoice.get("invoice_date")),
"posting_date": getdate(invoice.get("invoice_date")),
"service_date": getdate(invoice.get("service_date")),
"due_date": getdate(invoice.get("due_date")),
"payment_terms_template": invoice.get("invoice_terms"),
"remarks": invoice.get("notes"),
"owner": invoice.get("owner"),
"creation": invoice.get("creation"),
}
)
for item in invoice.get("supplier_invoice_items"):
invoice_doc.append(
"items",
{
"item_code": item.get("service_type"),
"received_qty": item.get("quantity"),
"qty": item.get("quantity"),
"rate": item.get("rate"),
},
)
# to avoid validating the custom due dates based on payment terms
invoice_doc.ignore_default_payment_terms_template = True
try:
invoice_doc.insert(ignore_permissions=True)
except Exception as e:
print(f"Error inserting invoice {invoice_doc.bill_no}: {e}")
continue
if invoice.get("street"):
address = create_address(
{
**invoice,
"title": invoice.get("supplier"),
"ref_doctype": "Supplier",
"ref_name": invoice.get("supplier"),
}
)
invoice_doc.supplier_address = address.name
invoice_doc.save()
try:
invoice_doc.submit()
except Exception as e:
print(f"Error submitting invoice {invoice_doc.bill_no}: {e}")
continue
for payment in invoice.get("payment_references"):
payment_doc = frappe.new_doc("Payment Entry")
payment_doc.update(
{
"payment_type": "Pay",
"posting_date": getdate(payment.get("payment_reference_date")),
"party_type": "Supplier",
"party": invoice.get("supplier"),
"paid_amount": payment.get("paid_amount"),
"received_amount": payment.get("paid_amount"),
"paid_from": get_account_name("Bank", "Asset"),
"reference_no": payment.get("payment_reference_number")
or "Note: Missing reference during ERPNext import",
"reference_date": getdate(
payment.get("payment_reference_date")
),
"references": [
{
"reference_doctype": invoice_doc.doctype,
"reference_name": invoice_doc.name,
"allocated_amount": payment.get("paid_amount"),
}
],
}
)
try:
payment_doc.insert(ignore_permissions=True)
payment_doc.submit()
except Exception as e:
print(
f"Error importing payment for invoice {invoice_doc.bill_no}: {e}"
)

View file

@ -2,35 +2,33 @@ import frappe
def execute():
update_supplier_invoices()
update_invoice_status()
update_supplier_invoices()
update_invoice_status()
def update_supplier_invoices():
suppliers = frappe.get_list("Supplier", pluck="name")
suppliers = frappe.get_list("Supplier", pluck="name")
for supplier in suppliers:
supplier_doc = frappe.get_doc("Supplier", supplier)
invoices = frappe.get_list(
"Invoices", filters={"supplier": supplier}, pluck="name"
)
if len(invoices) == 0:
continue
for supplier in suppliers:
supplier_doc = frappe.get_doc("Supplier", supplier)
invoices = frappe.get_list("Invoices", filters={"supplier": supplier}, pluck="name")
if len(invoices) == 0:
continue
supplier_doc.set("invoices", [])
for invoice in invoices:
supplier_doc.append("invoices", {"invoice_number": invoice})
supplier_doc.set("invoices", [])
for invoice in invoices:
supplier_doc.append("invoices", {"invoice_number": invoice})
supplier_doc.save()
supplier_doc.save()
frappe.db.commit()
frappe.db.commit()
def update_invoice_status():
invoices = frappe.get_list("Invoices", pluck="name")
invoices = frappe.get_list("Invoices", pluck="name")
for invoice in invoices:
invoice_doc = frappe.get_doc("Invoices", invoice)
invoice_doc.validate()
for invoice in invoices:
invoice_doc = frappe.get_doc("Invoices", invoice)
invoice_doc.validate()
frappe.db.commit()
frappe.db.commit()

View file

@ -1 +1 @@
import{_ as x,R as g,S as _,N as l,w as r,g as c,o,T as p,U as C,V as k,m as a,e as d,z as u,W as m,F as h,G as b}from"./vendor.99304cc5.js";const v={name:"FontColor",props:["editor"],components:{Popover:g,Tooltip:_},methods:{setBackgroundColor(t){t.name!="Default"?this.editor.chain().focus().toggleHighlight({color:t.hex}).run():this.editor.chain().focus().unsetHighlight().run()},setForegroundColor(t){t.name!="Default"?this.editor.chain().focus().setColor(t.hex).run():this.editor.chain().focus().unsetColor().run()}},computed:{foregroundColors(){return[{name:"Default",hex:"#1F272E"},{name:"Yellow",hex:"#ca8a04"},{name:"Orange",hex:"#ea580c"},{name:"Red",hex:"#dc2626"},{name:"Green",hex:"#16a34a"},{name:"Blue",hex:"#1579D0"},{name:"Purple",hex:"#9333ea"},{name:"Pink",hex:"#db2777"}]},backgroundColors(){return[{name:"Default",hex:null},{name:"Yellow",hex:"#fef9c3"},{name:"Orange",hex:"#ffedd5"},{name:"Red",hex:"#fee2e2"},{name:"Green",hex:"#dcfce7"},{name:"Blue",hex:"#D3E9FC"},{name:"Purple",hex:"#f3e8ff"},{name:"Pink",hex:"#fce7f3"}]}}},y={class:"p-2"},B=a("div",{class:"text-sm text-gray-700"},"Text Color",-1),P={class:"mt-1 grid grid-cols-8 gap-1"},F=["aria-label","onClick"],w=a("div",{class:"mt-2 text-sm text-gray-700"},"Background Color",-1),D={class:"mt-1 grid grid-cols-8 gap-1"},T=["aria-label","onClick"];function z(t,R,$,j,A,n){const i=c("Tooltip"),f=c("Popover");return o(),l(f,{transition:"default"},{target:r(({togglePopover:e,isOpen:s})=>[p(t.$slots,"default",C(k({onClick:()=>e(),isActive:s})))]),"body-main":r(()=>[a("div",y,[B,a("div",P,[(o(!0),d(h,null,u(n.foregroundColors,e=>(o(),l(i,{class:"flex",key:e.name,text:e.name},{default:r(()=>[a("button",{"aria-label":e.name,class:"flex h-5 w-5 items-center justify-center rounded border text-base",style:m({color:e.hex}),onClick:s=>n.setForegroundColor(e)}," A ",12,F)]),_:2},1032,["text"]))),128))]),w,a("div",D,[(o(!0),d(h,null,u(n.backgroundColors,e=>(o(),l(i,{class:"flex",key:e.name,text:e.name},{default:r(()=>[a("button",{"aria-label":e.name,class:b(["flex h-5 w-5 items-center justify-center rounded border text-base text-gray-900",e.hex?"border-transparent":"border-gray-200"]),style:m({backgroundColor:e.hex}),onClick:s=>n.setBackgroundColor(e)}," A ",14,T)]),_:2},1032,["text"]))),128))])])]),_:3})}var G=x(v,[["render",z]]);export{G as default};
import{_ as x,R as g,S as _,N as l,w as r,g as d,o,T as p,U as C,V as b,m as a,e as c,z as u,W as m,F as h,G as k}from"./vendor.3e69a9be.js";const v={name:"FontColor",props:["editor"],components:{Popover:g,Tooltip:_},methods:{setBackgroundColor(t){t.name!="Default"?this.editor.chain().focus().toggleHighlight({color:t.hex}).run():this.editor.chain().focus().unsetHighlight().run()},setForegroundColor(t){t.name!="Default"?this.editor.chain().focus().setColor(t.hex).run():this.editor.chain().focus().unsetColor().run()}},computed:{foregroundColors(){return[{name:"Default",hex:"#1F272E"},{name:"Yellow",hex:"#ca8a04"},{name:"Orange",hex:"#ea580c"},{name:"Red",hex:"#dc2626"},{name:"Green",hex:"#16a34a"},{name:"Blue",hex:"#1579D0"},{name:"Purple",hex:"#9333ea"},{name:"Pink",hex:"#db2777"}]},backgroundColors(){return[{name:"Default",hex:null},{name:"Yellow",hex:"#fef9c3"},{name:"Orange",hex:"#ffedd5"},{name:"Red",hex:"#fee2e2"},{name:"Green",hex:"#dcfce7"},{name:"Blue",hex:"#D3E9FC"},{name:"Purple",hex:"#f3e8ff"},{name:"Pink",hex:"#fce7f3"}]}}},y={class:"p-2"},B=a("div",{class:"text-sm text-gray-700"},"Text Color",-1),P={class:"mt-1 grid grid-cols-8 gap-1"},F=["aria-label","onClick"],w=a("div",{class:"mt-2 text-sm text-gray-700"},"Background Color",-1),D={class:"mt-1 grid grid-cols-8 gap-1"},T=["aria-label","onClick"];function z(t,R,$,j,A,n){const i=d("Tooltip"),f=d("Popover");return o(),l(f,{transition:"default"},{target:r(({togglePopover:e,isOpen:s})=>[p(t.$slots,"default",C(b({onClick:()=>e(),isActive:s})))]),"body-main":r(()=>[a("div",y,[B,a("div",P,[(o(!0),c(h,null,u(n.foregroundColors,e=>(o(),l(i,{class:"flex",key:e.name,text:e.name},{default:r(()=>[a("button",{"aria-label":e.name,class:"flex h-5 w-5 items-center justify-center rounded border text-base",style:m({color:e.hex}),onClick:s=>n.setForegroundColor(e)}," A ",12,F)]),_:2},1032,["text"]))),128))]),w,a("div",D,[(o(!0),c(h,null,u(n.backgroundColors,e=>(o(),l(i,{class:"flex",key:e.name,text:e.name},{default:r(()=>[a("button",{"aria-label":e.name,class:k(["flex h-5 w-5 items-center justify-center rounded border text-base text-gray-900",e.hex?"border-transparent":"border-gray-200"]),style:m({backgroundColor:e.hex}),onClick:s=>n.setBackgroundColor(e)}," A ",14,T)]),_:2},1032,["text"]))),128))])])]),_:3})}var G=x(v,[["render",z]]);export{G as default};

View file

@ -1 +1 @@
import{_ as f,j as I,Y as _,$ as h,e as g,T as D,U as v,V as y,f as n,w as l,F as w,g as m,o as c,m as i,t as C,p as V,v as u}from"./vendor.99304cc5.js";const b={name:"InsertImage",props:["editor"],expose:["openDialog"],data(){return{addImageDialog:{url:"",file:null,show:!1}}},components:{Button:I,Dialog:_},methods:{openDialog(){this.addImageDialog.show=!0},onImageSelect(t){let e=t.target.files[0];!e||(this.addImageDialog.file=e,h(e).then(r=>{this.addImageDialog.url=r}))},addImage(t){this.editor.chain().focus().setImage({src:t}).run(),this.reset()},reset(){this.addImageDialog=this.$options.data().addImageDialog}}},k={class:"relative cursor-pointer rounded-lg bg-gray-100 py-1 focus-within:bg-gray-200 hover:bg-gray-200"},x={class:"absolute inset-0 select-none px-2 py-1 text-base"},B=["src"],S=u(" Insert Image "),N=u(" Cancel ");function A(t,e,r,T,a,o){const d=m("Button"),p=m("Dialog");return c(),g(w,null,[D(t.$slots,"default",v(y({onClick:o.openDialog}))),n(p,{options:{title:"Add Image"},modelValue:a.addImageDialog.show,"onUpdate:modelValue":e[2]||(e[2]=s=>a.addImageDialog.show=s),onAfterLeave:o.reset},{"body-content":l(()=>[i("label",k,[i("input",{type:"file",class:"w-full opacity-0",onChange:e[0]||(e[0]=(...s)=>o.onImageSelect&&o.onImageSelect(...s)),accept:"image/*"},null,32),i("span",x,C(a.addImageDialog.file?"Select another image":"Select an image"),1)]),a.addImageDialog.url?(c(),g("img",{key:0,src:a.addImageDialog.url,class:"mt-2 w-full rounded-lg"},null,8,B)):V("",!0)]),actions:l(()=>[n(d,{variant:"solid",onClick:e[1]||(e[1]=s=>o.addImage(a.addImageDialog.url))},{default:l(()=>[S]),_:1}),n(d,{onClick:o.reset},{default:l(()=>[N]),_:1},8,["onClick"])]),_:1},8,["modelValue","onAfterLeave"])],64)}var F=f(b,[["render",A]]);export{F as default};
import{_ as f,j as I,Y as _,$ as h,e as g,T as D,U as v,V as y,f as n,w as l,F as w,g as m,o as c,m as i,t as C,p as b,v as u}from"./vendor.3e69a9be.js";const V={name:"InsertImage",props:["editor"],expose:["openDialog"],data(){return{addImageDialog:{url:"",file:null,show:!1}}},components:{Button:I,Dialog:_},methods:{openDialog(){this.addImageDialog.show=!0},onImageSelect(t){let e=t.target.files[0];!e||(this.addImageDialog.file=e,h(e).then(r=>{this.addImageDialog.url=r}))},addImage(t){this.editor.chain().focus().setImage({src:t}).run(),this.reset()},reset(){this.addImageDialog=this.$options.data().addImageDialog}}},k={class:"relative cursor-pointer rounded-lg bg-gray-100 py-1 focus-within:bg-gray-200 hover:bg-gray-200"},x={class:"absolute inset-0 select-none px-2 py-1 text-base"},B=["src"],S=u(" Insert Image "),N=u(" Cancel ");function A(t,e,r,T,a,o){const d=m("Button"),p=m("Dialog");return c(),g(w,null,[D(t.$slots,"default",v(y({onClick:o.openDialog}))),n(p,{options:{title:"Add Image"},modelValue:a.addImageDialog.show,"onUpdate:modelValue":e[2]||(e[2]=s=>a.addImageDialog.show=s),onAfterLeave:o.reset},{"body-content":l(()=>[i("label",k,[i("input",{type:"file",class:"w-full opacity-0",onChange:e[0]||(e[0]=(...s)=>o.onImageSelect&&o.onImageSelect(...s)),accept:"image/*"},null,32),i("span",x,C(a.addImageDialog.file?"Select another image":"Select an image"),1)]),a.addImageDialog.url?(c(),g("img",{key:0,src:a.addImageDialog.url,class:"mt-2 w-full rounded-lg"},null,8,B)):b("",!0)]),actions:l(()=>[n(d,{variant:"solid",onClick:e[1]||(e[1]=s=>o.addImage(a.addImageDialog.url))},{default:l(()=>[S]),_:1}),n(d,{onClick:o.reset},{default:l(()=>[N]),_:1},8,["onClick"])]),_:1},8,["modelValue","onAfterLeave"])],64)}var F=f(V,[["render",A]]);export{F as default};

View file

@ -1 +1 @@
import{_ as d,j as g,X as m,Y as L,e as f,T as p,U as c,V as D,f as i,w as l,F as h,g as a,o as _,Z as v,v as w}from"./vendor.99304cc5.js";const V={name:"InsertLink",props:["editor"],components:{Button:g,FormControl:m,Dialog:L},data(){return{setLinkDialog:{url:"",show:!1}}},methods:{openDialog(){let t=this.editor.getAttributes("link").href;t&&(this.setLinkDialog.url=t),this.setLinkDialog.show=!0},setLink(t){t===""?this.editor.chain().focus().extendMarkRange("link").unsetLink().run():this.editor.chain().focus().extendMarkRange("link").setLink({href:t}).run(),this.setLinkDialog.show=!1,this.setLinkDialog.url=""},reset(){this.setLinkDialog=this.$options.data().setLinkDialog}}},x=w(" Save ");function C(t,e,B,F,n,s){const r=a("FormControl"),u=a("Button"),k=a("Dialog");return _(),f(h,null,[p(t.$slots,"default",c(D({onClick:s.openDialog}))),i(k,{options:{title:"Set Link"},modelValue:n.setLinkDialog.show,"onUpdate:modelValue":e[3]||(e[3]=o=>n.setLinkDialog.show=o),onAfterLeave:s.reset},{"body-content":l(()=>[i(r,{type:"text",label:"URL",modelValue:n.setLinkDialog.url,"onUpdate:modelValue":e[0]||(e[0]=o=>n.setLinkDialog.url=o),onKeydown:e[1]||(e[1]=v(o=>s.setLink(o.target.value),["enter"]))},null,8,["modelValue"])]),actions:l(()=>[i(u,{variant:"solid",onClick:e[2]||(e[2]=o=>s.setLink(n.setLinkDialog.url))},{default:l(()=>[x]),_:1})]),_:1},8,["modelValue","onAfterLeave"])],64)}var U=d(V,[["render",C]]);export{U as default};
import{_ as d,j as g,X as m,Y as L,e as f,T as p,U as D,V as c,f as i,w as l,F as h,g as a,o as _,Z as v,v as w}from"./vendor.3e69a9be.js";const V={name:"InsertLink",props:["editor"],components:{Button:g,FormControl:m,Dialog:L},data(){return{setLinkDialog:{url:"",show:!1}}},methods:{openDialog(){let t=this.editor.getAttributes("link").href;t&&(this.setLinkDialog.url=t),this.setLinkDialog.show=!0},setLink(t){t===""?this.editor.chain().focus().extendMarkRange("link").unsetLink().run():this.editor.chain().focus().extendMarkRange("link").setLink({href:t}).run(),this.setLinkDialog.show=!1,this.setLinkDialog.url=""},reset(){this.setLinkDialog=this.$options.data().setLinkDialog}}},x=w(" Save ");function C(t,e,B,F,n,s){const r=a("FormControl"),u=a("Button"),k=a("Dialog");return _(),f(h,null,[p(t.$slots,"default",D(c({onClick:s.openDialog}))),i(k,{options:{title:"Set Link"},modelValue:n.setLinkDialog.show,"onUpdate:modelValue":e[3]||(e[3]=o=>n.setLinkDialog.show=o),onAfterLeave:s.reset},{"body-content":l(()=>[i(r,{type:"text",label:"URL",modelValue:n.setLinkDialog.url,"onUpdate:modelValue":e[0]||(e[0]=o=>n.setLinkDialog.url=o),onKeydown:e[1]||(e[1]=v(o=>s.setLink(o.target.value),["enter"]))},null,8,["modelValue"])]),actions:l(()=>[i(u,{variant:"solid",onClick:e[2]||(e[2]=o=>s.setLink(n.setLinkDialog.url))},{default:l(()=>[x]),_:1})]),_:1},8,["modelValue","onAfterLeave"])],64)}var U=d(V,[["render",C]]);export{U as default};

View file

@ -1 +1 @@
import{_ as D,j as v,Y as h,a0 as C,e as u,T as k,U as w,V as x,f as t,w as i,F as B,g as r,o as c,m as U,v as n,t as y,N as F,p}from"./vendor.99304cc5.js";const N={name:"InsertImage",props:["editor"],expose:["openDialog"],data(){return{addVideoDialog:{url:"",file:null,show:!1}}},components:{Button:v,Dialog:h,FileUploader:C},methods:{openDialog(){this.addVideoDialog.show=!0},onVideoSelect(l){let o=l.target.files[0];!o||(this.addVideoDialog.file=o)},addVideo(l){this.editor.chain().focus().insertContent(`<video src="${l}"></video>`).run(),this.reset()},reset(){this.addVideoDialog=this.$options.data().addVideoDialog}}},I={class:"flex items-center space-x-2"},S=n(" Remove "),A=["src"],j=n(" Insert Video "),L=n("Cancel");function P(l,o,R,T,e,s){const a=r("Button"),V=r("FileUploader"),g=r("Dialog");return c(),u(B,null,[k(l.$slots,"default",w(x({onClick:s.openDialog}))),t(g,{options:{title:"Add Video"},modelValue:e.addVideoDialog.show,"onUpdate:modelValue":o[2]||(o[2]=d=>e.addVideoDialog.show=d),onAfterLeave:s.reset},{"body-content":i(()=>[t(V,{"file-types":"video/*",onSuccess:o[0]||(o[0]=d=>e.addVideoDialog.url=d.file_url)},{default:i(({file:d,progress:f,uploading:_,openFileSelector:m})=>[U("div",I,[t(a,{onClick:m},{default:i(()=>[n(y(_?`Uploading ${f}%`:e.addVideoDialog.url?"Change Video":"Upload Video"),1)]),_:2},1032,["onClick"]),e.addVideoDialog.url?(c(),F(a,{key:0,onClick:()=>{e.addVideoDialog.url=null,e.addVideoDialog.file=null}},{default:i(()=>[S]),_:2},1032,["onClick"])):p("",!0)])]),_:1}),e.addVideoDialog.url?(c(),u("video",{key:0,src:e.addVideoDialog.url,class:"mt-2 w-full rounded-lg",type:"video/mp4",controls:""},null,8,A)):p("",!0)]),actions:i(()=>[t(a,{variant:"solid",onClick:o[1]||(o[1]=d=>s.addVideo(e.addVideoDialog.url))},{default:i(()=>[j]),_:1}),t(a,{onClick:s.reset},{default:i(()=>[L]),_:1},8,["onClick"])]),_:1},8,["modelValue","onAfterLeave"])],64)}var z=D(N,[["render",P]]);export{z as default};
import{_ as D,j as v,Y as h,a0 as C,e as u,T as k,U as w,V as x,f as t,w as i,F as B,g as r,o as c,m as U,v as n,t as y,N as F,p}from"./vendor.3e69a9be.js";const N={name:"InsertImage",props:["editor"],expose:["openDialog"],data(){return{addVideoDialog:{url:"",file:null,show:!1}}},components:{Button:v,Dialog:h,FileUploader:C},methods:{openDialog(){this.addVideoDialog.show=!0},onVideoSelect(l){let o=l.target.files[0];!o||(this.addVideoDialog.file=o)},addVideo(l){this.editor.chain().focus().insertContent(`<video src="${l}"></video>`).run(),this.reset()},reset(){this.addVideoDialog=this.$options.data().addVideoDialog}}},I={class:"flex items-center space-x-2"},S=n(" Remove "),A=["src"],b=n(" Insert Video "),j=n("Cancel");function L(l,o,P,R,e,a){const s=r("Button"),V=r("FileUploader"),g=r("Dialog");return c(),u(B,null,[k(l.$slots,"default",w(x({onClick:a.openDialog}))),t(g,{options:{title:"Add Video"},modelValue:e.addVideoDialog.show,"onUpdate:modelValue":o[2]||(o[2]=d=>e.addVideoDialog.show=d),onAfterLeave:a.reset},{"body-content":i(()=>[t(V,{"file-types":"video/*",onSuccess:o[0]||(o[0]=d=>e.addVideoDialog.url=d.file_url)},{default:i(({file:d,progress:f,uploading:_,openFileSelector:m})=>[U("div",I,[t(s,{onClick:m},{default:i(()=>[n(y(_?`Uploading ${f}%`:e.addVideoDialog.url?"Change Video":"Upload Video"),1)]),_:2},1032,["onClick"]),e.addVideoDialog.url?(c(),F(s,{key:0,onClick:()=>{e.addVideoDialog.url=null,e.addVideoDialog.file=null}},{default:i(()=>[S]),_:2},1032,["onClick"])):p("",!0)])]),_:1}),e.addVideoDialog.url?(c(),u("video",{key:0,src:e.addVideoDialog.url,class:"mt-2 w-full rounded-lg",type:"video/mp4",controls:""},null,8,A)):p("",!0)]),actions:i(()=>[t(s,{variant:"solid",onClick:o[1]||(o[1]=d=>a.addVideo(e.addVideoDialog.url))},{default:i(()=>[b]),_:1}),t(s,{onClick:a.reset},{default:i(()=>[j]),_:1},8,["onClick"])]),_:1},8,["modelValue","onAfterLeave"])],64)}var z=D(N,[["render",L]]);export{z as default};

View file

@ -1 +1 @@
import{l as f,r as g,e as r,f as o,w as m,g as i,o as n,m as w,n as x,u as t,t as h,p as y,q as b,v}from"./vendor.99304cc5.js";import{s}from"./index.87704181.js";const V={class:"m-3 flex flex-row items-center justify-center"},A=["onSubmit"],C={key:0},D={key:1},S=v("Login"),k={key:2,class:"text-red-500"},N=f({setup(B){const a=g({forAdmin:!1});function p(d){let e=new FormData(d.target);a.forAdmin?s.login.submit({email:e.get("email"),password:e.get("password")}):s.supplier_login.submit({supplier_id:e.get("supplier_id")})}return(d,e)=>{const l=i("Input"),u=i("Button"),c=i("Card");return n(),r("div",V,[o(c,{title:"Welcome to Supplier Portal!",class:"w-full max-w-md mt-4"},{default:m(()=>[w("form",{class:"flex flex-col space-y-2 w-full",onSubmit:b(p,["prevent"])},[o(x,{modelValue:t(a).forAdmin,"onUpdate:modelValue":e[0]||(e[0]=_=>t(a).forAdmin=_),label:"Login as Admin"},null,8,["modelValue"]),t(a).forAdmin?(n(),r("div",C,[o(l,{required:"",name:"email",type:"text",placeholder:"username@email.com",label:"User ID"}),o(l,{required:"",name:"password",type:"password",placeholder:"\u2022\u2022\u2022\u2022\u2022\u2022",label:"Password"})])):(n(),r("div",D,[o(l,{required:"",name:"supplier_id",type:"text",placeholder:"Supplier ID",label:"Supplier ID"})])),o(u,{loading:t(s).login.loading,appearance:"primary"},{default:m(()=>[S]),_:1},8,["loading"]),t(s).login.error?(n(),r("div",k,h(t(s).login.error),1)):y("",!0)],40,A)]),_:1})])}}});export{N as default};
import{l as f,r as g,e as r,f as o,w as m,g as i,o as n,m as w,n as x,u as t,t as b,p as h,q as y,v}from"./vendor.3e69a9be.js";import{s}from"./index.a459834f.js";const V={class:"m-3 flex flex-row items-center justify-center"},A=["onSubmit"],C={key:0},D={key:1},S=v("Login"),k={key:2,class:"text-red-500"},N=f({setup(B){const a=g({forAdmin:!1});function p(d){let e=new FormData(d.target);a.forAdmin?s.login.submit({email:e.get("email"),password:e.get("password")}):s.supplier_login.submit({supplier_id:e.get("supplier_id")})}return(d,e)=>{const l=i("Input"),u=i("Button"),c=i("Card");return n(),r("div",V,[o(c,{title:"Welcome to Supplier Portal!",class:"w-full max-w-md mt-4"},{default:m(()=>[w("form",{class:"flex flex-col space-y-2 w-full",onSubmit:y(p,["prevent"])},[o(x,{modelValue:t(a).forAdmin,"onUpdate:modelValue":e[0]||(e[0]=_=>t(a).forAdmin=_),label:"Login as Admin"},null,8,["modelValue"]),t(a).forAdmin?(n(),r("div",C,[o(l,{required:"",name:"email",type:"text",placeholder:"username@email.com",label:"User ID"}),o(l,{required:"",name:"password",type:"password",placeholder:"\u2022\u2022\u2022\u2022\u2022\u2022",label:"Password"})])):(n(),r("div",D,[o(l,{required:"",name:"supplier_id",type:"text",placeholder:"Supplier ID",label:"Supplier ID"})])),o(u,{loading:t(s).login.loading,appearance:"primary"},{default:m(()=>[S]),_:1},8,["loading"]),t(s).login.error?(n(),r("div",k,b(t(s).login.error),1)):h("",!0)],40,A)]),_:1})])}}});export{N as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
input.form-control-plaintext[data-v-7312333a]:focus,.currency-fmt[data-v-7312333a]{background-color:#fff;border-color:#fff;box-shadow:none}.currency-fmt+input.form-control[data-v-7312333a]{background-color:#fff;border-color:#fff;box-shadow:none}tr[data-v-7312333a]{vertical-align:middle}

View file

@ -0,0 +1 @@
input.form-control-plaintext[data-v-0c596e92]:focus,.currency-fmt[data-v-0c596e92]{background-color:#fff;border-color:#fff;box-shadow:none}.currency-fmt+input.form-control[data-v-0c596e92]{background-color:#fff;border-color:#fff;box-shadow:none}tr[data-v-0c596e92]{vertical-align:middle}

View file

@ -0,0 +1 @@
.vr-aligned[data-v-5c4ded13]{vertical-align:middle}button.accordion-button[aria-expanded=true][data-v-5c4ded13]{color:#000}input.form-control-plaintext[data-v-5c4ded13]:focus,.input-group>input.form-control[data-v-5c4ded13]:focus,input.form-control[data-v-5c4ded13],.input-group>span[data-v-5c4ded13]{background-color:#fff;border-color:#fff;box-shadow:none}.disabled-date[data-v-5c4ded13]{color:#000;background:transparent;border:none!important;border-bottom:1px solid #ced4da!important}a[data-v-5c4ded13]{text-decoration:none;color:inherit}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
.vr-aligned[data-v-145c3c9a]{vertical-align:middle}button.accordion-button[aria-expanded=true][data-v-145c3c9a]{color:#000}input.form-control-plaintext[data-v-145c3c9a]:focus,.input-group>input.form-control[data-v-145c3c9a]:focus,input.form-control[data-v-145c3c9a],.input-group>span[data-v-145c3c9a]{background-color:#fff;border-color:#fff;box-shadow:none}.disabled-date[data-v-145c3c9a]{color:#000;background:transparent;border:none!important;border-bottom:1px solid #ced4da!important}a[data-v-145c3c9a]{text-decoration:none;color:inherit}

View file

@ -0,0 +1 @@
a[data-v-570396b7]{text-decoration:none;color:inherit}

View file

@ -1 +0,0 @@
import{_ as y,x as k,y as h,e as a,m as t,f as i,w as l,u as s,F as I,z as w,p as b,A as B,B as S,g,o as c,v as r,t as p,D as N}from"./vendor.99304cc5.js";import{s as d}from"./index.87704181.js";const _=u=>(B("data-v-6f6873f7"),u=u(),S(),u),L={class:"container-fluid"},C={class:"container mx-auto mt-5"},P={class:"row"},V=_(()=>t("div",{class:"col"},[t("h1",{class:"text-gray-900 font-bold text-[32px]"},"Invoices"),t("div",{class:"d-flex"},[t("div",null,[t("div",{class:"btn-group",role:"group","aria-label":"Basic example"})])])],-1)),j={class:"col"},$={class:"d-flex"},A={class:"ms-auto d-flex",style:{gap:"2px"}},D=_(()=>t("button",{type:"button",class:"btn btn-primary btn-dark"},"New",-1)),F={key:0,class:"container mx-auto mb-3 mt-5"},z={class:"table"},E=_(()=>t("thead",null,[t("tr",null,[t("th",{scope:"col"},"Invoice Number"),t("th",{scope:"col"},"Supplier"),t("th",{scope:"col"},"Status"),t("th",{scope:"col"},"Amount")])],-1)),M={class:"table-group-divider"},R=_(()=>t("i",{class:"bi bi-currency-dollar"},null,-1)),T={key:0,class:"flex justify-between mt-4"},q=r(" Previous Page "),G=r(" Next Page "),H={setup(u){k(()=>{window.location.origin==="https://accounting.seahavenind.com"&&window.location.replace("https://accounting.seahavenind.com/app")});let v={doctype:"Invoices",fields:["*"],orderBy:"creation desc",start:0,pageLength:5,auto:!0},o=null;return d.supplier_id&&d.isLoggedIn?(v.filters=[["supplier","=",d.supplier_id]],o=h(v)):o=h(v),o.fetch(),(J,n)=>{const m=g("router-link"),f=g("Button");return c(),a("div",L,[t("div",C,[t("div",P,[V,t("div",j,[t("div",$,[t("div",A,[i(m,{to:"/invoices/new"},{default:l(()=>[D]),_:1}),t("button",{type:"button",class:"btn btn-primary btn-dark",onClick:n[0]||(n[0]=(...e)=>s(d).logout.submit&&s(d).logout.submit(...e))}," Logout ")])])])]),s(o).data?(c(),a("div",F,[t("table",z,[E,t("tbody",M,[(c(!0),a(I,null,w(s(o).data,(e,x)=>(c(),a("tr",{key:x},[t("td",null,[i(m,{to:{name:"SupplierInvoice",params:{supplierInvoiceNumber:e.name}}},{default:l(()=>[r(p(e.name),1)]),_:2},1032,["to"])]),t("td",null,p(e.supplier),1),t("td",null,[i(s(N),null,{default:l(()=>[r(p(e.status),1)]),_:2},1024)]),t("td",null,[R,r(" "+p(e.total_amount),1)])]))),128))])])])):b("",!0)]),s(o).length>10?(c(),a("div",T,[i(f,{onClick:n[1]||(n[1]=e=>s(o).previous())},{default:l(()=>[q]),_:1}),i(f,{onClick:n[2]||(n[2]=e=>s(o).next())},{default:l(()=>[G]),_:1})])):b("",!0)])}}};var Q=y(H,[["__scopeId","data-v-6f6873f7"]]);export{Q as default};

View file

@ -0,0 +1 @@
import{_ as y,x as k,y as h,e as a,m as t,f as i,w as l,u as o,F as I,z as w,p as f,A as B,B as S,g,o as c,v as r,t as p,D as N}from"./vendor.3e69a9be.js";import{s as d}from"./index.a459834f.js";const _=u=>(B("data-v-570396b7"),u=u(),S(),u),L={class:"container-fluid"},C={class:"container mx-auto mt-5"},P={class:"row"},V=_(()=>t("div",{class:"col"},[t("h1",{class:"text-gray-900 font-bold text-[32px]"},"Invoices"),t("div",{class:"d-flex"},[t("div",null,[t("div",{class:"btn-group",role:"group","aria-label":"Basic example"})])])],-1)),j={class:"col"},$={class:"d-flex"},A={class:"ms-auto d-flex",style:{gap:"2px"}},D=_(()=>t("button",{type:"button",class:"btn btn-primary btn-dark"},"New",-1)),F={key:0,class:"container mx-auto mb-3 mt-5"},z={class:"table"},E=_(()=>t("thead",null,[t("tr",null,[t("th",{scope:"col"},"Invoice Number"),t("th",{scope:"col"},"Supplier"),t("th",{scope:"col"},"Status"),t("th",{scope:"col"},"Amount")])],-1)),M={class:"table-group-divider"},R=_(()=>t("i",{class:"bi bi-currency-dollar"},null,-1)),T={key:0,class:"flex justify-between mt-4"},q=r(" Previous Page "),G=r(" Next Page "),H={setup(u){k(()=>{window.location.origin==="https://accounting.seahavenind.com"&&window.location.replace("https://accounting.seahavenind.com/app")});let v={doctype:"Purchase Invoice",fields:["*"],orderBy:"creation desc",start:0,pageLength:5,auto:!0},s=null;return d.supplier_id&&d.isLoggedIn?(v.filters=[["supplier","=",d.supplier_id]],s=h(v)):s=h(v),s.fetch(),(J,n)=>{const m=g("router-link"),b=g("Button");return c(),a("div",L,[t("div",C,[t("div",P,[V,t("div",j,[t("div",$,[t("div",A,[i(m,{to:"/invoices/new"},{default:l(()=>[D]),_:1}),t("button",{type:"button",class:"btn btn-primary btn-dark",onClick:n[0]||(n[0]=(...e)=>o(d).logout.submit&&o(d).logout.submit(...e))},"Logout")])])])]),o(s).data?(c(),a("div",F,[t("table",z,[E,t("tbody",M,[(c(!0),a(I,null,w(o(s).data,(e,x)=>(c(),a("tr",{key:x},[t("td",null,[i(m,{to:{name:"SupplierInvoice",params:{supplierInvoiceNumber:e.name}}},{default:l(()=>[r(p(e.name),1)]),_:2},1032,["to"])]),t("td",null,p(e.supplier),1),t("td",null,[i(o(N),null,{default:l(()=>[r(p(e.status),1)]),_:2},1024)]),t("td",null,[R,r(" "+p(e.grand_total),1)])]))),128))])])])):f("",!0)]),o(s).length>10?(c(),a("div",T,[i(b,{onClick:n[1]||(n[1]=e=>o(s).previous())},{default:l(()=>[q]),_:1}),i(b,{onClick:n[2]||(n[2]=e=>o(s).next())},{default:l(()=>[G]),_:1})])):f("",!0)])}}};var Q=y(H,[["__scopeId","data-v-570396b7"]]);export{Q as default};

View file

@ -1 +0,0 @@
a[data-v-6f6873f7]{text-decoration:none;color:inherit}

View file

@ -1 +0,0 @@
var h=(e,o,n)=>new Promise((s,r)=>{var t=p=>{try{a(n.next(p))}catch(g){r(g)}},c=p=>{try{a(n.throw(p))}catch(g){r(g)}},a=p=>p.done?s(p.value):Promise.resolve(p.value).then(t,c);a((n=n.apply(e,o)).next())});import{c as m,r as L,a as I,b as k,d as S,_ as y,e as E,f as P,g as b,o as C,h as R,s as T,i as w,j as $,C as U,I as j,k as O}from"./vendor.99304cc5.js";const A=function(){const o=document.createElement("link").relList;if(o&&o.supports&&o.supports("modulepreload"))return;for(const r of document.querySelectorAll('link[rel="modulepreload"]'))s(r);new MutationObserver(r=>{for(const t of r)if(t.type==="childList")for(const c of t.addedNodes)c.tagName==="LINK"&&c.rel==="modulepreload"&&s(c)}).observe(document,{childList:!0,subtree:!0});function n(r){const t={};return r.integrity&&(t.integrity=r.integrity),r.referrerpolicy&&(t.referrerPolicy=r.referrerpolicy),r.crossorigin==="use-credentials"?t.credentials="include":r.crossorigin==="anonymous"?t.credentials="omit":t.credentials="same-origin",t}function s(r){if(r.ep)return;r.ep=!0;const t=n(r);fetch(r.href,t)}};A();const x="modulepreload",v={},N="/assets/suppliers_portal/supplier-portal/",d=function(o,n){return!n||n.length===0?o():Promise.all(n.map(s=>{if(s=`${N}${s}`,s in v)return;v[s]=!0;const r=s.endsWith(".css"),t=r?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${s}"]${t}`))return;const c=document.createElement("link");if(c.rel=r?"stylesheet":x,r||(c.as="script",c.crossOrigin=""),c.href=s,document.head.appendChild(c),r)return new Promise((a,p)=>{c.addEventListener("load",a),c.addEventListener("error",p)})})).then(()=>o())},_=m({url:"frappe.auth.get_logged_user",cache:"User",onError(e){e&&e.exc_type==="AuthenticationError"&&u.push({name:"LoginPage"})}});function f(){let o=new URLSearchParams(document.cookie.split("; ").join("&")).get("user_id");return o==="Guest"&&(o=null),o}function V(){return new URLSearchParams(document.cookie.split("; ").join("&")).get("supplier_id")}function D(){return new URLSearchParams(document.cookie.split("; ").join("&")).get("supplier_name")}const i=L({supplier_login:m({url:"suppliers_portal.suppliers_portal.api.validate_supplier_id",makeParams({supplier_id:e}){return{supplier_id:e}},onSuccess(e){e&&e.status==="success"&&(_.reload(),i.user=f(),document.cookie=`supplier_id=${e.supplier_id}`,document.cookie=`supplier_name=${e.supplier_name}`,i.supplier_id=e.supplier_id,i.supplier_name=e.supplier_name,i.login.reset(),u.replace({name:"SupplierInvoiceList"}))},onError(e){console.error("Login failed",e)}}),login:m({url:"login",makeParams({email:e,password:o}){return{usr:e,pwd:o}},onSuccess(e){e&&(_.reload(),i.user=f(),document.cookie=`supplier_id=${i.supplier_id}; expires=Thu, 01 Jan 1970 00:00:00 UTC'`,document.cookie=`supplier_id=${i.supplier_name}; expires=Thu, 01 Jan 1970 00:00:00 UTC'`,i.supplier_id="",i.supplier_name="",i.login.reset(),u.replace({name:"SupplierInvoiceList"}))},onError(e){console.error("Login failed",e)}}),logout:m({url:"logout",onSuccess(){document.cookie=`supplier_id=${i.supplier_id}; expires=Thu, 01 Jan 1970 00:00:00 UTC'`,document.cookie=`supplier_id=${i.supplier_name}; expires=Thu, 01 Jan 1970 00:00:00 UTC'`,i.supplier_id="",i.supplier_name="",_.reset(),i.user=f(),u.replace({name:"Login"})}}),user:f(),supplier_id:V(),supplier_name:D(),isLoggedIn:I(()=>!!i.user)}),J=[{name:"Login",path:"/login",component:()=>d(()=>import("./Login.14c1625b.js"),["assets/Login.14c1625b.js","assets/vendor.99304cc5.js","assets/vendor.52e2d7c1.css"])},{name:"SupplierInvoiceList",path:"/",component:()=>d(()=>import("./SupplierInvoiceList.bd20c4e8.js"),["assets/SupplierInvoiceList.bd20c4e8.js","assets/SupplierInvoiceList.e8de1e21.css","assets/vendor.99304cc5.js","assets/vendor.52e2d7c1.css"])},{name:"SupplierInvoice",path:"/invoices/:supplierInvoiceNumber",component:()=>d(()=>import("./SupplierInvoice.77908b92.js"),["assets/SupplierInvoice.77908b92.js","assets/SupplierInvoice.9dfb3ca8.css","assets/vendor.99304cc5.js","assets/vendor.52e2d7c1.css"]),props:!0},{name:"SupplierInvoiceCreate",path:"/invoices/new",component:()=>d(()=>import("./NewInvoice.632d4fad.js"),["assets/NewInvoice.632d4fad.js","assets/NewInvoice.67c196f0.css","assets/vendor.99304cc5.js","assets/vendor.52e2d7c1.css"])}];let u=k({history:S("/supplier-portal"),routes:J});u.beforeEach((e,o,n)=>h(void 0,null,function*(){let s=i.isLoggedIn;try{yield _.promise}catch(r){s=!1}console.log("isLoggedIn",s),console.log("to",e),console.log("from",o),e.name==="Login"&&s?n({name:"SupplierInvoiceList"}):e.name!=="Login"&&!s?n({name:"Login"}):n()}));const q={};function B(e,o){const n=b("router-view");return C(),E("div",null,[P(n)])}var F=y(q,[["render",B]]);let l=R(F);T("resourceFetcher",O);l.use(u);l.use(w);l.component("Button",$);l.component("Card",U);l.component("Input",j);l.mount("#app");export{f as a,V as b,D as c,u as r,i as s};

View file

@ -0,0 +1 @@
var h=(e,s,i)=>new Promise((n,r)=>{var t=c=>{try{l(i.next(c))}catch(g){r(g)}},p=c=>{try{l(i.throw(c))}catch(g){r(g)}},l=c=>c.done?n(c.value):Promise.resolve(c.value).then(t,p);l((i=i.apply(e,s)).next())});import{c as m,r as L,a as k,b as I,d as S,_ as y,e as E,f as P,g as b,o as C,h as R,s as T,i as w,j as $,C as U,I as j,k as O}from"./vendor.3e69a9be.js";const A=function(){const s=document.createElement("link").relList;if(s&&s.supports&&s.supports("modulepreload"))return;for(const r of document.querySelectorAll('link[rel="modulepreload"]'))n(r);new MutationObserver(r=>{for(const t of r)if(t.type==="childList")for(const p of t.addedNodes)p.tagName==="LINK"&&p.rel==="modulepreload"&&n(p)}).observe(document,{childList:!0,subtree:!0});function i(r){const t={};return r.integrity&&(t.integrity=r.integrity),r.referrerpolicy&&(t.referrerPolicy=r.referrerpolicy),r.crossorigin==="use-credentials"?t.credentials="include":r.crossorigin==="anonymous"?t.credentials="omit":t.credentials="same-origin",t}function n(r){if(r.ep)return;r.ep=!0;const t=i(r);fetch(r.href,t)}};A();const x="modulepreload",v={},N="/assets/suppliers_portal/supplier-portal/",d=function(s,i){return!i||i.length===0?s():Promise.all(i.map(n=>{if(n=`${N}${n}`,n in v)return;v[n]=!0;const r=n.endsWith(".css"),t=r?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${n}"]${t}`))return;const p=document.createElement("link");if(p.rel=r?"stylesheet":x,r||(p.as="script",p.crossOrigin=""),p.href=n,document.head.appendChild(p),r)return new Promise((l,c)=>{p.addEventListener("load",l),p.addEventListener("error",c)})})).then(()=>s())},_=m({url:"frappe.auth.get_logged_user",cache:"User",onError(e){e&&e.exc_type==="AuthenticationError"&&u.push({name:"LoginPage"})}});function f(){let s=new URLSearchParams(document.cookie.split("; ").join("&")).get("user_id");return s==="Guest"&&(s=null),s}function V(){return new URLSearchParams(document.cookie.split("; ").join("&")).get("supplier_id")}function D(){return new URLSearchParams(document.cookie.split("; ").join("&")).get("supplier_name")}const o=L({supplier_login:m({url:"suppliers_portal.suppliers_portal.api.validate_supplier_id",makeParams({supplier_id:e}){return{supplier_id:e}},onSuccess(e){e&&e.status==="success"&&(_.reload(),o.user=f(),document.cookie=`supplier_id=${e.supplier_id}`,document.cookie=`supplier_name=${e.supplier_name}`,o.supplier_id=e.supplier_id,o.supplier_name=e.supplier_name,o.login.reset(),u.replace({name:"SupplierInvoiceList"}))},onError(e){console.error("Login failed",e)}}),login:m({url:"login",makeParams({email:e,password:s}){return{usr:e,pwd:s}},onSuccess(e){e&&(_.reload(),o.user=f(),document.cookie=`supplier_id=${o.supplier_id}; expires=Thu, 01 Jan 1970 00:00:00 UTC'`,document.cookie=`supplier_id=${o.supplier_name}; expires=Thu, 01 Jan 1970 00:00:00 UTC'`,o.supplier_id="",o.supplier_name="",o.login.reset(),u.replace({name:"SupplierInvoiceList"}))},onError(e){console.error("Login failed",e)}}),logout:m({url:"logout",onSuccess(){document.cookie=`supplier_id=${o.supplier_id}; expires=Thu, 01 Jan 1970 00:00:00 UTC'`,document.cookie=`supplier_id=${o.supplier_name}; expires=Thu, 01 Jan 1970 00:00:00 UTC'`,o.supplier_id="",o.supplier_name="",_.reset(),o.user=f(),u.replace({name:"Login"})}}),user:f(),supplier_id:V(),supplier_name:D(),isLoggedIn:k(()=>!!o.user)}),J=[{name:"Login",path:"/login",component:()=>d(()=>import("./Login.2ab92036.js"),["assets/Login.2ab92036.js","assets/vendor.3e69a9be.js","assets/vendor.43acabc4.css"])},{name:"SupplierInvoiceList",path:"/",component:()=>d(()=>import("./SupplierInvoiceList.d20068c5.js"),["assets/SupplierInvoiceList.d20068c5.js","assets/SupplierInvoiceList.0fe6f3df.css","assets/vendor.3e69a9be.js","assets/vendor.43acabc4.css"])},{name:"SupplierInvoice",path:"/invoices/:supplierInvoiceNumber",component:()=>d(()=>import("./SupplierInvoice.6fa41666.js"),["assets/SupplierInvoice.6fa41666.js","assets/SupplierInvoice.062d709e.css","assets/vendor.3e69a9be.js","assets/vendor.43acabc4.css"]),props:!0},{name:"SupplierInvoiceCreate",path:"/invoices/new",component:()=>d(()=>import("./NewInvoice.15ff173a.js"),["assets/NewInvoice.15ff173a.js","assets/NewInvoice.e56a66d4.css","assets/vendor.3e69a9be.js","assets/vendor.43acabc4.css"])}];let u=I({history:S("/supplier-portal"),routes:J});u.beforeEach((e,s,i)=>h(void 0,null,function*(){let n=o.isLoggedIn;try{yield _.promise}catch(r){n=!1}e.name==="Login"&&n?i({name:"SupplierInvoiceList"}):e.name!=="Login"&&!n?i({name:"Login"}):i()}));const q={};function B(e,s){const i=b("router-view");return C(),E("div",null,[P(i)])}var F=y(q,[["render",B]]);let a=R(F);T("resourceFetcher",O);a.use(u);a.use(w);a.component("Button",$);a.component("Card",U);a.component("Input",j);a.mount("#app");export{f as a,V as b,D as c,u as r,o as s};

View file

@ -5,9 +5,9 @@
<link rel="icon" href="/assets/suppliers_portal/supplier-portal/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Supplier Portal</title>
<script type="module" crossorigin src="/assets/suppliers_portal/supplier-portal/assets/index.87704181.js"></script>
<link rel="modulepreload" href="/assets/suppliers_portal/supplier-portal/assets/vendor.99304cc5.js">
<link rel="stylesheet" href="/assets/suppliers_portal/supplier-portal/assets/vendor.52e2d7c1.css">
<script type="module" crossorigin src="/assets/suppliers_portal/supplier-portal/assets/index.a459834f.js"></script>
<link rel="modulepreload" href="/assets/suppliers_portal/supplier-portal/assets/vendor.3e69a9be.js">
<link rel="stylesheet" href="/assets/suppliers_portal/supplier-portal/assets/vendor.43acabc4.css">
<link rel="stylesheet" href="/assets/suppliers_portal/supplier-portal/assets/index.e4753eeb.css">
</head>
<body>

View file

@ -3,18 +3,18 @@ import frappe
@frappe.whitelist(allow_guest=True)
def validate_supplier_id(supplier_id: str) -> dict:
if frappe.db.exists("Supplier", {"name": supplier_id}):
user = frappe.get_doc("User", f"{supplier_id}@supplier-portal.com")
portal_supplier = frappe.get_doc("Supplier", supplier_id)
# login user
frappe.local.login_manager.user = user.name
frappe.local.login_manager.post_login()
return {
"status": "success",
"message": "User logged in successfully",
"supplier_id": supplier_id,
"supplier_name": portal_supplier.company_name,
}
else:
frappe.local.response["http_status_code"] = 404
frappe.throw("Supplier not found", frappe.AuthenticationError)
if frappe.db.exists("Supplier", {"name": supplier_id}):
user = frappe.get_doc("User", f"{supplier_id}@supplier-portal.com")
portal_supplier = frappe.get_doc("Supplier", supplier_id)
# login user
frappe.local.login_manager.user = user.name
frappe.local.login_manager.post_login()
return {
"status": "success",
"message": "User logged in successfully",
"supplier_id": supplier_id,
"supplier_name": portal_supplier.company_name,
}
else:
frappe.local.response["http_status_code"] = 404
frappe.throw("Supplier not found", frappe.AuthenticationError)

View file

@ -0,0 +1,137 @@
{
"custom_fields": [
{
"_assign": null,
"_comments": null,
"_liked_by": null,
"_user_tags": null,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"creation": "2025-05-19 02:22:24.828657",
"default": null,
"depends_on": null,
"description": null,
"docstatus": 0,
"dt": "Purchase Invoice",
"fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "service_date",
"fieldtype": "Date",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"idx": 12,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"insert_after": "due_date",
"is_system_generated": 0,
"is_virtual": 0,
"label": "Service Date",
"length": 0,
"link_filters": null,
"mandatory_depends_on": null,
"modified": "2025-05-19 02:22:24.828657",
"modified_by": "Administrator",
"module": null,
"name": "Purchase Invoice-service_date",
"no_copy": 0,
"non_negative": 0,
"options": null,
"owner": "Administrator",
"permlevel": 0,
"placeholder": null,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"read_only_depends_on": null,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"show_dashboard": 0,
"sort_options": 0,
"translatable": 0,
"unique": 0,
"width": null
},
{
"_assign": null,
"_comments": null,
"_liked_by": null,
"_user_tags": null,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"creation": "2025-05-19 02:22:24.721092",
"default": null,
"depends_on": null,
"description": null,
"docstatus": 0,
"dt": "Purchase Invoice",
"fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "site_code",
"fieldtype": "Data",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"idx": 6,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"insert_after": "company",
"is_system_generated": 0,
"is_virtual": 0,
"label": "Site Code",
"length": 0,
"link_filters": null,
"mandatory_depends_on": null,
"modified": "2025-05-19 02:22:24.721092",
"modified_by": "Administrator",
"module": null,
"name": "Purchase Invoice-site_code",
"no_copy": 0,
"non_negative": 0,
"options": null,
"owner": "Administrator",
"permlevel": 0,
"placeholder": null,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"read_only_depends_on": null,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"show_dashboard": 0,
"sort_options": 0,
"translatable": 1,
"unique": 0,
"width": null
}
],
"custom_perms": [],
"doctype": "Purchase Invoice",
"links": [],
"property_setters": [],
"sync_on_migrate": 1
}

View file

@ -1,47 +0,0 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2024-10-09 16:28:25.879209",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"payment_reference_number",
"payment_reference_date",
"column_break_nukw",
"paid_amount"
],
"fields": [
{
"fieldname": "payment_reference_number",
"fieldtype": "Data",
"label": "Reference Number"
},
{
"fieldname": "payment_reference_date",
"fieldtype": "Date",
"label": "Payment Date"
},
{
"fieldname": "column_break_nukw",
"fieldtype": "Column Break"
},
{
"fieldname": "paid_amount",
"fieldtype": "Currency",
"label": "Amount"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-10-12 07:16:26.416034",
"modified_by": "Administrator",
"module": "Suppliers Portal",
"name": "Invoice Payment References",
"owner": "Administrator",
"permissions": [],
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View file

@ -1,9 +0,0 @@
# Copyright (c) 2024, jeowsome15@gmail.com and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class InvoicePaymentReferences(Document):
pass

View file

@ -1,8 +0,0 @@
// Copyright (c) 2024, jeowsome15@gmail.com and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Invoices", {
// refresh(frm) {
// },
// });

View file

@ -1,216 +0,0 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:supplier_invoice_number",
"creation": "2024-09-24 02:44:02.596278",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"supplier_invoice_number",
"supplier",
"supplier_name",
"column_break_yjtl",
"service_date",
"invoice_date",
"invoice_terms",
"due_date",
"status",
"section_break_swkm",
"supplier_invoice_items",
"section_break_slsq",
"payment_references",
"section_break_ajbe",
"paid_amount",
"amount_due",
"total_amount",
"column_break_quxe",
"section_break_olmx",
"service_address",
"site_code",
"street",
"city",
"state",
"zip_code",
"column_break_hvmc",
"notes"
],
"fields": [
{
"fieldname": "supplier",
"fieldtype": "Link",
"label": "Supplier",
"options": "Supplier"
},
{
"fieldname": "supplier_invoice_number",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Supplier Invoice Number",
"reqd": 1,
"unique": 1
},
{
"default": "Due On Receipt",
"fieldname": "invoice_terms",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Invoice Terms",
"options": "NET 10\nNET 15\nNET 30\nNET 45\nNET 60\nDue On Receipt",
"reqd": 1
},
{
"fieldname": "column_break_yjtl",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_swkm",
"fieldtype": "Section Break"
},
{
"fieldname": "supplier_invoice_items",
"fieldtype": "Table",
"label": "Invoice Items",
"options": "Supplier Invoice Items"
},
{
"default": "Unpaid",
"fieldname": "status",
"fieldtype": "Select",
"hidden": 1,
"label": "Status",
"options": "Paid\nUnpaid\nOverdue\nPartially Paid\nPartially Paid and Overdue"
},
{
"fieldname": "service_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Service Date",
"reqd": 1
},
{
"fieldname": "section_break_olmx",
"fieldtype": "Section Break"
},
{
"fieldname": "notes",
"fieldtype": "Small Text",
"label": "Notes"
},
{
"fetch_from": "supplier.company_name",
"fieldname": "supplier_name",
"fieldtype": "Data",
"hidden": 1,
"label": "Supplier Name",
"read_only": 1
},
{
"fieldname": "service_address",
"fieldtype": "Small Text",
"hidden": 1,
"label": "Service Address"
},
{
"fieldname": "section_break_ajbe",
"fieldtype": "Section Break"
},
{
"fieldname": "total_amount",
"fieldtype": "Currency",
"label": "Total Amount",
"read_only": 1
},
{
"fieldname": "column_break_quxe",
"fieldtype": "Column Break"
},
{
"fieldname": "street",
"fieldtype": "Data",
"label": "Street"
},
{
"fieldname": "city",
"fieldtype": "Data",
"label": "City"
},
{
"fieldname": "state",
"fieldtype": "Data",
"label": "State"
},
{
"fieldname": "zip_code",
"fieldtype": "Data",
"label": "Zip Code"
},
{
"fieldname": "column_break_hvmc",
"fieldtype": "Column Break"
},
{
"fieldname": "invoice_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Invoice Date",
"reqd": 1
},
{
"fieldname": "due_date",
"fieldtype": "Date",
"label": "Due Date"
},
{
"fieldname": "site_code",
"fieldtype": "Data",
"label": "Site Code"
},
{
"fieldname": "section_break_slsq",
"fieldtype": "Section Break"
},
{
"fieldname": "payment_references",
"fieldtype": "Table",
"label": "Payment References",
"options": "Invoice Payment References"
},
{
"fieldname": "paid_amount",
"fieldtype": "Currency",
"label": "Paid Amount",
"read_only": 1
},
{
"fieldname": "amount_due",
"fieldtype": "Currency",
"label": "Amount Due",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-10-12 08:12:37.273898",
"modified_by": "Administrator",
"module": "Suppliers Portal",
"name": "Invoices",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View file

@ -1,90 +0,0 @@
# Copyright (c) 2024, jeowsome15@gmail.com and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
from frappe.utils import add_days, flt, getdate, nowdate
class Invoices(Document):
def validate(self):
self.set_due_date()
self.set_total_amount()
self.set_total_paid()
self.update_amount_due()
self.update_status()
def on_update(self):
self.update_suppliers_invoice()
def set_total_amount(self):
if self.get("supplier_invoice_items"):
total_amount = 0
for item in self.get("supplier_invoice_items"):
item.amount = flt(item.rate) * flt(item.quantity)
total_amount += item.amount
self.db_set("total_amount", total_amount)
def set_due_date(self):
if self.invoice_terms:
if self.invoice_terms == "NET 10":
self.due_date = add_days(self.invoice_date, 10)
elif self.invoice_terms == "NET 15":
self.due_date = add_days(self.invoice_date, 15)
elif self.invoice_terms == "NET 30":
self.due_date = add_days(self.invoice_date, 30)
elif self.invoice_terms == "NET 45":
self.due_date = add_days(self.invoice_date, 45)
elif self.invoice_terms == "NET 60":
self.due_date = add_days(self.invoice_date, 60)
def set_total_paid(self):
total_paid = 0
for item in self.payment_references:
total_paid += item.paid_amount
self.db_set("paid_amount", total_paid)
def update_status(self):
if self.paid_amount == 0:
self.status = "Unpaid"
elif self.paid_amount < self.total_amount and self.paid_amount > 0:
self.status = "Partially Paid"
elif self.paid_amount == self.total_amount:
self.status = "Paid"
if getdate(nowdate()) > getdate(self.due_date):
if self.paid_amount == 0:
self.status = "Overdue"
elif self.paid_amount < self.total_amount:
self.status = "Partially Paid and Overdue"
self.db_set("status", self.status, update_modified=False, commit=True)
def update_amount_due(self):
self.db_set(
"amount_due",
flt(self.total_amount) - flt(self.paid_amount),
update_modified=False,
commit=True,
)
def update_suppliers_invoice(self):
supplier = frappe.get_doc("Supplier", self.supplier)
# check if invoice already exists
for invoice in supplier.invoices:
if invoice.invoice_number == self.name:
if invoice.total_due != self.total_amount:
invoice.total_amount = self.total_amount
if invoice.invoice_date != self.invoice_date:
invoice.invoice_date = self.invoice_date
if invoice.status != self.status:
invoice.status = self.status
supplier.save()
return
supplier.append(
"invoices",
{"invoice_number": self.name},
)
supplier.save()

View file

@ -1,5 +0,0 @@
frappe.listview_settings['Invoices'] = {
before_render() {
$('button[data-label="Add Invoices"]>span>span').html('Add Invoice')
},
}

View file

@ -1,9 +0,0 @@
# Copyright (c) 2024, jeowsome15@gmail.com and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestInvoices(FrappeTestCase):
pass

View file

@ -1,8 +0,0 @@
// Copyright (c) 2024, jeowsome15@gmail.com and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Supplier", {
// refresh(frm) {
// },
// });

View file

@ -1,100 +0,0 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2024-09-24 02:39:16.353285",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"company_name",
"column_break_ykyo",
"company_address",
"user",
"street",
"city",
"state",
"zip_code",
"section_break_svzz",
"invoices"
],
"fields": [
{
"fieldname": "company_name",
"fieldtype": "Data",
"label": "Company Name"
},
{
"fieldname": "company_address",
"fieldtype": "Small Text",
"hidden": 1,
"label": "Company Address"
},
{
"fieldname": "user",
"fieldtype": "Link",
"hidden": 1,
"label": "User",
"options": "User"
},
{
"fieldname": "column_break_ykyo",
"fieldtype": "Column Break"
},
{
"fieldname": "street",
"fieldtype": "Data",
"label": "Street"
},
{
"fieldname": "city",
"fieldtype": "Data",
"label": "City"
},
{
"fieldname": "state",
"fieldtype": "Data",
"label": "State"
},
{
"fieldname": "zip_code",
"fieldtype": "Data",
"label": "Zip Code"
},
{
"fieldname": "section_break_svzz",
"fieldtype": "Section Break"
},
{
"fieldname": "invoices",
"fieldtype": "Table",
"label": "Invoices",
"options": "Supplier Invoices",
"read_only": 1
}
],
"links": [],
"modified": "2024-10-09 18:09:28.675273",
"modified_by": "Administrator",
"module": "Suppliers Portal",
"name": "Supplier",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"search_fields": "company_name",
"show_title_field_in_link": 1,
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "company_name"
}

View file

@ -1,38 +0,0 @@
# Copyright (c) 2024, jeowsome15@gmail.com and contributors
# For license information, please see license.txt
import random
import frappe
from frappe.model.document import Document
class Supplier(Document):
def generate_supplier_id(self, length: int = 8) -> str:
# generate unique supplier integer id based on length
supplier_id = random.randint(10 ** (length - 1), 10**length - 1)
while frappe.db.exists("Supplier", {"supplier_id": supplier_id}):
supplier_id = random.randint(10 ** (length - 1), 10**length - 1)
return str(supplier_id)
def autoname(self):
self.name = self.generate_supplier_id()
def after_insert(self):
self.create_user()
def create_user(self):
user = frappe.new_doc("User")
user.update(
{
"email": f"{self.name}@supplier-portal.com",
"first_name": f"Supplier {self.name}",
"roles": [{"role": "Portal Supplier"}],
"new_password": self.name,
}
)
user.insert()
self.user = user.name
self.save()

View file

@ -1,7 +0,0 @@
{% extends "templates/web.html" %}
{% block page_content %}
<h1>{{ title |e }}</h1>
{% endblock %}
<!-- this is a sample default web page template -->

View file

@ -1,4 +0,0 @@
<div>
<a href="/{{ doc.route |e }}">{{ (doc.title or doc.name) |e }}</a>
</div>
<!-- this is a sample default list template -->

View file

@ -1,9 +0,0 @@
# Copyright (c) 2024, jeowsome15@gmail.com and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestSupplier(FrappeTestCase):
pass

View file

@ -1,56 +0,0 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2024-09-24 02:47:44.354811",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"service_type",
"column_break_ocxt",
"quantity",
"rate",
"amount"
],
"fields": [
{
"fieldname": "service_type",
"fieldtype": "Select",
"label": "Service Type",
"options": "Repair\nPreventive Maintence"
},
{
"fieldname": "column_break_ocxt",
"fieldtype": "Column Break"
},
{
"default": "1",
"fieldname": "quantity",
"fieldtype": "Float",
"label": "Quantity"
},
{
"fieldname": "rate",
"fieldtype": "Currency",
"label": "Rate"
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"label": "Amount",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-09-28 00:59:33.037008",
"modified_by": "Administrator",
"module": "Suppliers Portal",
"name": "Supplier Invoice Items",
"owner": "Administrator",
"permissions": [],
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View file

@ -1,9 +0,0 @@
# Copyright (c) 2024, jeowsome15@gmail.com and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class SupplierInvoiceItems(Document):
pass

View file

@ -1,69 +0,0 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2024-10-09 16:59:58.079331",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"invoice_number",
"status",
"column_break_mprs",
"invoice_date",
"terms",
"total_due"
],
"fields": [
{
"fieldname": "invoice_number",
"fieldtype": "Link",
"label": "Invoice Number",
"options": "Invoices",
"read_only": 1
},
{
"fetch_from": "invoice_number.invoice_date",
"fieldname": "invoice_date",
"fieldtype": "Date",
"label": "Invoice Date",
"read_only": 1
},
{
"fetch_from": "invoice_number.invoice_terms",
"fieldname": "terms",
"fieldtype": "Data",
"label": "Term",
"read_only": 1
},
{
"fetch_from": "invoice_number.total_amount",
"fieldname": "total_due",
"fieldtype": "Currency",
"label": "Total Due",
"read_only": 1
},
{
"fetch_from": "invoice_number.status",
"fieldname": "status",
"fieldtype": "Data",
"label": "Status",
"read_only": 1
},
{
"fieldname": "column_break_mprs",
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-10-09 17:33:59.785852",
"modified_by": "Administrator",
"module": "Suppliers Portal",
"name": "Supplier Invoices",
"owner": "Administrator",
"permissions": [],
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}

View file

@ -1,9 +0,0 @@
# Copyright (c) 2024, jeowsome15@gmail.com and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class SupplierInvoices(Document):
pass

View file

@ -1,30 +0,0 @@
frappe.query_reports["Supplier Invoices"] = {
"filters": [
{
"fieldname": "invoice_date_from",
"label": __("Invoice Date From"),
"fieldtype": "Date",
"default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), // Default to last month
"placeholder": __("Invoice Date From")
},
{
"fieldname": "invoice_date_to",
"label": __("Invoice Date To"),
"fieldtype": "Date",
"default": frappe.datetime.get_today(), // Default to today
},
{
"fieldname": "supplier",
"label": __("Supplier"),
"fieldtype": "Link",
"options": "Supplier"
},
{
"fieldname": "status",
"label": __("Status"),
"fieldtype": "Select",
"options": "All\nPaid\nUnpaid\nOverdue",
"default": "All",
}
]
};

View file

@ -1,30 +0,0 @@
{
"add_total_row": 1,
"columns": [],
"creation": "2024-10-09 15:22:34.256149",
"disabled": 1,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"letterhead": null,
"modified": "2024-10-09 17:03:58.993246",
"modified_by": "Administrator",
"module": "Suppliers Portal",
"name": "Supplier Invoices",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Invoices",
"report_name": "Supplier Invoices",
"report_type": "Script Report",
"roles": [
{
"role": "System Manager"
},
{
"role": "Portal Supplier"
}
],
"timeout": 0
}

View file

@ -1,49 +0,0 @@
import frappe
def execute(filters=None):
columns = [
{
"fieldname": "invoice_number",
"label": "Invoice Number",
"fieldtype": "Link",
"options": "Invoices",
},
{
"fieldname": "supplier_name",
"label": "Supplier Name",
"fieldtype": "Data",
"width": 200,
},
{"fieldname": "status", "label": "Status", "fieldtype": "Data", "width": 100},
{"fieldname": "invoice_date", "label": "Invoice Date", "fieldtype": "Date"},
{"fieldname": "due_date", "label": "Due Date", "fieldtype": "Date"},
{"fieldname": "total_amount", "label": "Total Amount", "fieldtype": "Currency"},
]
conditions = ""
if filters.get("invoice_date_from"):
conditions += " AND invoice_date >= %(invoice_date_from)s"
if filters.get("invoice_date_to"):
conditions += " AND invoice_date <= %(invoice_date_to)s"
if filters.get("supplier"):
conditions += " AND supplier = %(supplier)s"
if filters.get("status"):
if not filters.get("status") == "All":
conditions += " AND status = %(status)s"
query = f"""
SELECT
name as invoice_number,
supplier_name,
status,
invoice_date,
due_date,
total_amount
FROM
`tabInvoices`
WHERE
docstatus = 0 {conditions}
"""
data = frappe.db.sql(query, filters, as_dict=True)
return columns, data

View file

@ -1,63 +0,0 @@
{
"app": "supplier_portal",
"charts": [],
"content": "[{\"id\":\"AyiOTTupgl\",\"type\":\"paragraph\",\"data\":{\"text\":\"Documents\",\"col\":12}},{\"id\":\"64VHemT3p8\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Suppliers\",\"col\":4}},{\"id\":\"_vl927eRbo\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Invoices\",\"col\":4}},{\"id\":\"j428xL5Lje\",\"type\":\"paragraph\",\"data\":{\"text\":\"Reports\",\"col\":12}},{\"id\":\"CDzGMPClRq\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Supplier Invoices Report\",\"col\":4}}]",
"creation": "2024-10-02 17:23:06.804441",
"custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"for_user": "",
"hide_custom": 0,
"icon": "clipboard",
"idx": 0,
"indicator_color": "green",
"is_hidden": 0,
"label": "Supplier Portal",
"links": [],
"modified": "2024-10-12 07:25:56.798875",
"modified_by": "Administrator",
"module": "Suppliers Portal",
"name": "Supplier Portal",
"number_cards": [],
"owner": "Administrator",
"parent_page": "",
"public": 1,
"quick_lists": [],
"restrict_to_domain": "",
"roles": [
{
"role": "Portal Supplier"
},
{
"role": "Portal User"
}
],
"sequence_id": 2.0,
"shortcuts": [
{
"color": "Grey",
"doc_view": "List",
"label": "Supplier Invoices Report",
"link_to": "Supplier Invoices",
"type": "Report"
},
{
"color": "Grey",
"doc_view": "List",
"label": "Suppliers",
"link_to": "Supplier",
"stats_filter": "[]",
"type": "DocType"
},
{
"color": "Grey",
"doc_view": "List",
"label": "Invoices",
"link_to": "Invoices",
"stats_filter": "[]",
"type": "DocType"
}
],
"title": "Supplier Portal",
"type": "Workspace"
}

View file

@ -1,48 +0,0 @@
{
"app": "Suppliers Portal",
"charts": [],
"content": "[{\"id\":\"Lix3qbsE_H\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\">Suppliers Portal</span>\",\"col\":12}},{\"id\":\"OdX-YH0tro\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Supplier\",\"col\":4}},{\"id\":\"_vl927eRbo\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Invoices\",\"col\":4}}]",
"creation": "2024-10-02 17:23:06.804441",
"custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"for_user": "",
"hide_custom": 0,
"icon": "clipboard",
"idx": 0,
"indicator_color": "green",
"is_hidden": 0,
"label": "Suppliers Portal",
"links": [],
"modified": "2024-10-03 03:19:57.683322",
"modified_by": "Administrator",
"module": "Suppliers Portal",
"name": "Suppliers Portal",
"number_cards": [],
"owner": "Administrator",
"parent_page": "",
"public": 1,
"quick_lists": [],
"roles": [],
"sequence_id": 23.0,
"shortcuts": [
{
"color": "Grey",
"doc_view": "List",
"label": "Supplier",
"link_to": "Supplier",
"stats_filter": "[]",
"type": "DocType"
},
{
"color": "Grey",
"doc_view": "List",
"label": "Invoices",
"link_to": "Invoices",
"stats_filter": "[]",
"type": "DocType"
}
],
"title": "Suppliers Portal",
"type": "Workspace"
}

View file

@ -1,16 +0,0 @@
import frappe
def daily_invoice_status_update():
invoices = frappe.get_all(
"Invoices",
filters={
"status": ["in", ["Unpaid", "Partially Paid", "Partially Paid and Overdue"]]
},
fields=["name"],
)
for invoice in invoices:
invoice_doc = frappe.get_doc("Invoices", invoice.name)
invoice_doc.update_status()
invoice_doc.save()
frappe.db.commit()

View file

@ -5,9 +5,9 @@
<link rel="icon" href="/assets/suppliers_portal/supplier-portal/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Supplier Portal</title>
<script type="module" crossorigin src="/assets/suppliers_portal/supplier-portal/assets/index.87704181.js"></script>
<link rel="modulepreload" href="/assets/suppliers_portal/supplier-portal/assets/vendor.99304cc5.js">
<link rel="stylesheet" href="/assets/suppliers_portal/supplier-portal/assets/vendor.52e2d7c1.css">
<script type="module" crossorigin src="/assets/suppliers_portal/supplier-portal/assets/index.a459834f.js"></script>
<link rel="modulepreload" href="/assets/suppliers_portal/supplier-portal/assets/vendor.3e69a9be.js">
<link rel="stylesheet" href="/assets/suppliers_portal/supplier-portal/assets/vendor.43acabc4.css">
<link rel="stylesheet" href="/assets/suppliers_portal/supplier-portal/assets/index.e4753eeb.css">
</head>
<body>