Merge branch 'develop' of https://github.com/frappe/erpnext into nil_exempt_non_gst_gstr_1
This commit is contained in:
commit
1846dbfb4d
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -40,6 +40,7 @@ body:
|
|||||||
- HR
|
- HR
|
||||||
- projects
|
- projects
|
||||||
- support
|
- support
|
||||||
|
- CRM
|
||||||
- assets
|
- assets
|
||||||
- integrations
|
- integrations
|
||||||
- quality
|
- quality
|
||||||
@ -48,6 +49,7 @@ body:
|
|||||||
- agriculture
|
- agriculture
|
||||||
- education
|
- education
|
||||||
- non-profit
|
- non-profit
|
||||||
|
- other
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
55
.github/labeler.yml
vendored
Normal file
55
.github/labeler.yml
vendored
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
accounts:
|
||||||
|
- erpnext/accounts/*
|
||||||
|
- erpnext/controllers/accounts_controller.py
|
||||||
|
- erpnext/controllers/taxes_and_totals.py
|
||||||
|
|
||||||
|
stock:
|
||||||
|
- erpnext/stock/*
|
||||||
|
- erpnext/controllers/stock_controller.py
|
||||||
|
- erpnext/controllers/item_variant.py
|
||||||
|
|
||||||
|
assets:
|
||||||
|
- erpnext/assets/*
|
||||||
|
|
||||||
|
regional:
|
||||||
|
- erpnext/regional/*
|
||||||
|
|
||||||
|
selling:
|
||||||
|
- erpnext/selling/*
|
||||||
|
- erpnext/controllers/selling_controller.py
|
||||||
|
|
||||||
|
buying:
|
||||||
|
- erpnext/buying/*
|
||||||
|
- erpnext/controllers/buying_controller.py
|
||||||
|
|
||||||
|
support:
|
||||||
|
- erpnext/support/*
|
||||||
|
|
||||||
|
POS:
|
||||||
|
- pos*
|
||||||
|
|
||||||
|
ecommerce:
|
||||||
|
- erpnext/e_commerce/*
|
||||||
|
|
||||||
|
maintenance:
|
||||||
|
- erpnext/maintenance/*
|
||||||
|
|
||||||
|
manufacturing:
|
||||||
|
- erpnext/manufacturing/*
|
||||||
|
|
||||||
|
crm:
|
||||||
|
- erpnext/crm/*
|
||||||
|
|
||||||
|
HR:
|
||||||
|
- erpnext/hr/*
|
||||||
|
|
||||||
|
payroll:
|
||||||
|
- erpnext/payroll*
|
||||||
|
|
||||||
|
projects:
|
||||||
|
- erpnext/projects/*
|
||||||
|
|
||||||
|
# Any python files modifed but no test files modified
|
||||||
|
needs-tests:
|
||||||
|
- any: ['erpnext/**/*.py']
|
||||||
|
all: ['!erpnext/**/test*.py']
|
2
.github/workflows/docs-checker.yml
vendored
2
.github/workflows/docs-checker.yml
vendored
@ -12,7 +12,7 @@ jobs:
|
|||||||
- name: 'Setup Environment'
|
- name: 'Setup Environment'
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: 3.6
|
python-version: 3.8
|
||||||
|
|
||||||
- name: 'Clone repo'
|
- name: 'Clone repo'
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
12
.github/workflows/labeller.yml
vendored
Normal file
12
.github/workflows/labeller.yml
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
name: "Pull Request Labeler"
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened, reopened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
triage:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/labeler@v3
|
||||||
|
with:
|
||||||
|
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
2
.github/workflows/patch.yml
vendored
2
.github/workflows/patch.yml
vendored
@ -34,7 +34,7 @@ jobs:
|
|||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: 3.7
|
python-version: 3.8
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
|
2
.github/workflows/server-tests-mariadb.yml
vendored
2
.github/workflows/server-tests-mariadb.yml
vendored
@ -46,7 +46,7 @@ jobs:
|
|||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: 3.7
|
python-version: 3.8
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
|
2
.github/workflows/server-tests-postgres.yml
vendored
2
.github/workflows/server-tests-postgres.yml
vendored
@ -46,7 +46,7 @@ jobs:
|
|||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: 3.7
|
python-version: 3.8
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
|
2
.github/workflows/ui-tests.yml
vendored
2
.github/workflows/ui-tests.yml
vendored
@ -36,7 +36,7 @@ jobs:
|
|||||||
- name: Setup Python
|
- name: Setup Python
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: 3.7
|
python-version: 3.8
|
||||||
|
|
||||||
- uses: actions/setup-node@v2
|
- uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
|
@ -23,13 +23,13 @@ erpnext/stock/ @marination @rohitwaghchaure @ankush
|
|||||||
|
|
||||||
erpnext/crm/ @ruchamahabal @pateljannat
|
erpnext/crm/ @ruchamahabal @pateljannat
|
||||||
erpnext/education/ @ruchamahabal @pateljannat
|
erpnext/education/ @ruchamahabal @pateljannat
|
||||||
erpnext/healthcare/ @ruchamahabal @pateljannat @chillaranand
|
|
||||||
erpnext/hr/ @ruchamahabal @pateljannat
|
erpnext/hr/ @ruchamahabal @pateljannat
|
||||||
erpnext/non_profit/ @ruchamahabal
|
|
||||||
erpnext/payroll @ruchamahabal @pateljannat
|
erpnext/payroll @ruchamahabal @pateljannat
|
||||||
erpnext/projects/ @ruchamahabal @pateljannat
|
erpnext/projects/ @ruchamahabal @pateljannat
|
||||||
|
|
||||||
erpnext/controllers @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination
|
erpnext/controllers/ @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination @ankush
|
||||||
|
erpnext/patches/ @deepeshgarg007 @nextchamp-saqib @marination @ankush
|
||||||
|
erpnext/public/ @nextchamp-saqib @marination
|
||||||
|
|
||||||
.github/ @surajshetty3416 @ankush
|
.github/ @ankush
|
||||||
requirements.txt @gavindsouza
|
requirements.txt @gavindsouza
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
frappe.ui.form.on('Currency Exchange Settings', {
|
||||||
|
service_provider: function(frm) {
|
||||||
|
if (frm.doc.service_provider == "exchangerate.host") {
|
||||||
|
let result = ['result'];
|
||||||
|
let params = {
|
||||||
|
date: '{transaction_date}',
|
||||||
|
from: '{from_currency}',
|
||||||
|
to: '{to_currency}'
|
||||||
|
};
|
||||||
|
add_param(frm, "https://api.exchangerate.host/convert", params, result);
|
||||||
|
} else if (frm.doc.service_provider == "frankfurter.app") {
|
||||||
|
let result = ['rates', '{to_currency}'];
|
||||||
|
let params = {
|
||||||
|
base: '{from_currency}',
|
||||||
|
symbols: '{to_currency}'
|
||||||
|
};
|
||||||
|
add_param(frm, "https://frankfurter.app/{transaction_date}", params, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function add_param(frm, api, params, result) {
|
||||||
|
var row;
|
||||||
|
frm.clear_table("req_params");
|
||||||
|
frm.clear_table("result_key");
|
||||||
|
|
||||||
|
frm.doc.api_endpoint = api;
|
||||||
|
|
||||||
|
$.each(params, function(key, value) {
|
||||||
|
row = frm.add_child("req_params");
|
||||||
|
row.key = key;
|
||||||
|
row.value = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
$.each(result, function(key, value) {
|
||||||
|
row = frm.add_child("result_key");
|
||||||
|
row.key = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
frm.refresh_fields();
|
||||||
|
}
|
@ -0,0 +1,126 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2022-01-10 13:03:26.237081",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"api_details_section",
|
||||||
|
"service_provider",
|
||||||
|
"api_endpoint",
|
||||||
|
"url",
|
||||||
|
"column_break_3",
|
||||||
|
"help",
|
||||||
|
"section_break_2",
|
||||||
|
"req_params",
|
||||||
|
"column_break_4",
|
||||||
|
"result_key"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "api_details_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "API Details"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "api_endpoint",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "API Endpoint",
|
||||||
|
"read_only_depends_on": "eval: doc.service_provider != \"Custom\"",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "url",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Example URL",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_3",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "help",
|
||||||
|
"fieldtype": "HTML",
|
||||||
|
"label": "Help",
|
||||||
|
"options": "<h3>Currency Exchange Settings Help</h3>\n<p>There are 3 variables that could be used within the endpoint, result key and in values of the parameter.</p>\n<p>Exchange rate between {from_currency} and {to_currency} on {transaction_date} is fetched by the API.</p>\n<p>Example: If your endpoint is exchange.com/2021-08-01, then, you will have to input exchange.com/{transaction_date}</p>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_2",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Request Parameters"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "req_params",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Parameters",
|
||||||
|
"options": "Currency Exchange Settings Details",
|
||||||
|
"read_only_depends_on": "eval: doc.service_provider != \"Custom\"",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_4",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "result_key",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Result Key",
|
||||||
|
"options": "Currency Exchange Settings Result",
|
||||||
|
"read_only_depends_on": "eval: doc.service_provider != \"Custom\"",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "service_provider",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Service Provider",
|
||||||
|
"options": "frankfurter.app\nexchangerate.host\nCustom",
|
||||||
|
"reqd": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"issingle": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2022-01-10 15:51:14.521174",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "Currency Exchange Settings",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"role": "Accounts Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"role": "Accounts User",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
@ -0,0 +1,82 @@
|
|||||||
|
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
import requests
|
||||||
|
from frappe import _
|
||||||
|
from frappe.model.document import Document
|
||||||
|
from frappe.utils import nowdate
|
||||||
|
|
||||||
|
|
||||||
|
class CurrencyExchangeSettings(Document):
|
||||||
|
def validate(self):
|
||||||
|
self.set_parameters_and_result()
|
||||||
|
response, value = self.validate_parameters()
|
||||||
|
self.validate_result(response, value)
|
||||||
|
|
||||||
|
def set_parameters_and_result(self):
|
||||||
|
if self.service_provider == 'exchangerate.host':
|
||||||
|
self.set('result_key', [])
|
||||||
|
self.set('req_params', [])
|
||||||
|
|
||||||
|
self.api_endpoint = "https://api.exchangerate.host/convert"
|
||||||
|
self.append('result_key', {'key': 'result'})
|
||||||
|
self.append('req_params', {'key': 'date', 'value': '{transaction_date}'})
|
||||||
|
self.append('req_params', {'key': 'from', 'value': '{from_currency}'})
|
||||||
|
self.append('req_params', {'key': 'to', 'value': '{to_currency}'})
|
||||||
|
elif self.service_provider == 'frankfurter.app':
|
||||||
|
self.set('result_key', [])
|
||||||
|
self.set('req_params', [])
|
||||||
|
|
||||||
|
self.api_endpoint = "https://frankfurter.app/{transaction_date}"
|
||||||
|
self.append('result_key', {'key': 'rates'})
|
||||||
|
self.append('result_key', {'key': '{to_currency}'})
|
||||||
|
self.append('req_params', {'key': 'base', 'value': '{from_currency}'})
|
||||||
|
self.append('req_params', {'key': 'symbols', 'value': '{to_currency}'})
|
||||||
|
|
||||||
|
def validate_parameters(self):
|
||||||
|
if frappe.flags.in_test:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
params = {}
|
||||||
|
for row in self.req_params:
|
||||||
|
params[row.key] = row.value.format(
|
||||||
|
transaction_date=nowdate(),
|
||||||
|
to_currency='INR',
|
||||||
|
from_currency='USD'
|
||||||
|
)
|
||||||
|
|
||||||
|
api_url = self.api_endpoint.format(
|
||||||
|
transaction_date=nowdate(),
|
||||||
|
to_currency='INR',
|
||||||
|
from_currency='USD'
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(api_url, params=params)
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
frappe.throw("Error: " + str(e))
|
||||||
|
|
||||||
|
response.raise_for_status()
|
||||||
|
value = response.json()
|
||||||
|
|
||||||
|
return response, value
|
||||||
|
|
||||||
|
def validate_result(self, response, value):
|
||||||
|
if frappe.flags.in_test:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
for key in self.result_key:
|
||||||
|
value = value[str(key.key).format(
|
||||||
|
transaction_date=nowdate(),
|
||||||
|
to_currency='INR',
|
||||||
|
from_currency='USD'
|
||||||
|
)]
|
||||||
|
except Exception:
|
||||||
|
frappe.throw("Invalid result key. Response: " + response.text)
|
||||||
|
if not isinstance(value, (int, float)):
|
||||||
|
frappe.throw(_("Returned exchange rate is neither integer not float."))
|
||||||
|
|
||||||
|
self.url = response.url
|
||||||
|
frappe.msgprint("Exchange rate of USD to INR is " + str(value))
|
@ -0,0 +1,9 @@
|
|||||||
|
# Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
class TestCurrencyExchangeSettings(unittest.TestCase):
|
||||||
|
pass
|
@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2021-09-02 14:54:49.033512",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"key",
|
||||||
|
"value"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "key",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Key",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "value",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Value",
|
||||||
|
"reqd": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2021-11-03 19:14:55.889037",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "Currency Exchange Settings Details",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
# Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class CurrencyExchangeSettingsDetails(Document):
|
||||||
|
pass
|
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2021-09-03 13:17:22.088259",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"key"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "key",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Key",
|
||||||
|
"reqd": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2021-11-03 19:14:40.054245",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "Currency Exchange Settings Result",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
# Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class CurrencyExchangeSettingsResult(Document):
|
||||||
|
pass
|
@ -1708,7 +1708,10 @@ def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outsta
|
|||||||
|
|
||||||
def apply_early_payment_discount(paid_amount, received_amount, doc):
|
def apply_early_payment_discount(paid_amount, received_amount, doc):
|
||||||
total_discount = 0
|
total_discount = 0
|
||||||
if doc.doctype in ['Sales Invoice', 'Purchase Invoice'] and doc.payment_schedule:
|
eligible_for_payments = ['Sales Order', 'Sales Invoice', 'Purchase Order', 'Purchase Invoice']
|
||||||
|
has_payment_schedule = hasattr(doc, 'payment_schedule') and doc.payment_schedule
|
||||||
|
|
||||||
|
if doc.doctype in eligible_for_payments and has_payment_schedule:
|
||||||
for term in doc.payment_schedule:
|
for term in doc.payment_schedule:
|
||||||
if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date:
|
if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date:
|
||||||
if term.discount_type == 'Percentage':
|
if term.discount_type == 'Percentage':
|
||||||
|
@ -353,7 +353,6 @@ class POSInvoice(SalesInvoice):
|
|||||||
if not for_validate and not self.customer:
|
if not for_validate and not self.customer:
|
||||||
self.customer = profile.customer
|
self.customer = profile.customer
|
||||||
|
|
||||||
self.ignore_pricing_rule = profile.ignore_pricing_rule
|
|
||||||
self.account_for_change_amount = profile.get('account_for_change_amount') or self.account_for_change_amount
|
self.account_for_change_amount = profile.get('account_for_change_amount') or self.account_for_change_amount
|
||||||
self.set_warehouse = profile.get('warehouse') or self.set_warehouse
|
self.set_warehouse = profile.get('warehouse') or self.set_warehouse
|
||||||
|
|
||||||
|
@ -556,6 +556,37 @@ class TestPOSInvoice(unittest.TestCase):
|
|||||||
batch.cancel()
|
batch.cancel()
|
||||||
batch.delete()
|
batch.delete()
|
||||||
|
|
||||||
|
def test_ignore_pricing_rule(self):
|
||||||
|
from erpnext.accounts.doctype.pricing_rule.test_pricing_rule import make_pricing_rule
|
||||||
|
|
||||||
|
item_price = frappe.get_doc({
|
||||||
|
'doctype': 'Item Price',
|
||||||
|
'item_code': '_Test Item',
|
||||||
|
'price_list': '_Test Price List',
|
||||||
|
'price_list_rate': '450',
|
||||||
|
})
|
||||||
|
item_price.insert()
|
||||||
|
pr = make_pricing_rule(selling=1, priority=5, discount_percentage=10)
|
||||||
|
pr.save()
|
||||||
|
pos_inv = create_pos_invoice(qty=1, do_not_submit=1)
|
||||||
|
pos_inv.items[0].rate = 300
|
||||||
|
pos_inv.save()
|
||||||
|
self.assertEquals(pos_inv.items[0].discount_percentage, 10)
|
||||||
|
# rate shouldn't change
|
||||||
|
self.assertEquals(pos_inv.items[0].rate, 405)
|
||||||
|
|
||||||
|
pos_inv.ignore_pricing_rule = 1
|
||||||
|
pos_inv.items[0].rate = 300
|
||||||
|
pos_inv.save()
|
||||||
|
self.assertEquals(pos_inv.ignore_pricing_rule, 1)
|
||||||
|
# rate should change since pricing rules are ignored
|
||||||
|
self.assertEquals(pos_inv.items[0].rate, 300)
|
||||||
|
|
||||||
|
item_price.delete()
|
||||||
|
pos_inv.delete()
|
||||||
|
pr.delete()
|
||||||
|
|
||||||
|
|
||||||
def create_pos_invoice(**args):
|
def create_pos_invoice(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
pos_profile = None
|
pos_profile = None
|
||||||
|
@ -650,7 +650,7 @@ def make_pricing_rule(**args):
|
|||||||
"rate": args.rate or 0.0,
|
"rate": args.rate or 0.0,
|
||||||
"margin_rate_or_amount": args.margin_rate_or_amount or 0.0,
|
"margin_rate_or_amount": args.margin_rate_or_amount or 0.0,
|
||||||
"condition": args.condition or '',
|
"condition": args.condition or '',
|
||||||
"priority": 1,
|
"priority": args.priority or 1,
|
||||||
"discount_amount": args.discount_amount or 0.0,
|
"discount_amount": args.discount_amount or 0.0,
|
||||||
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0
|
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0
|
||||||
})
|
})
|
||||||
@ -676,6 +676,8 @@ def make_pricing_rule(**args):
|
|||||||
if args.get(applicable_for):
|
if args.get(applicable_for):
|
||||||
doc.db_set(applicable_for, args.get(applicable_for))
|
doc.db_set(applicable_for, args.get(applicable_for))
|
||||||
|
|
||||||
|
return doc
|
||||||
|
|
||||||
def setup_pricing_rule_data():
|
def setup_pricing_rule_data():
|
||||||
if not frappe.db.exists('Campaign', '_Test Campaign'):
|
if not frappe.db.exists('Campaign', '_Test Campaign'):
|
||||||
frappe.get_doc({
|
frappe.get_doc({
|
||||||
|
@ -651,7 +651,7 @@
|
|||||||
"hide_seconds": 1,
|
"hide_seconds": 1,
|
||||||
"label": "Ignore Pricing Rule",
|
"label": "Ignore Pricing Rule",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"permlevel": 1,
|
"permlevel": 0,
|
||||||
"print_hide": 1
|
"print_hide": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -2038,7 +2038,7 @@
|
|||||||
"link_fieldname": "consolidated_invoice"
|
"link_fieldname": "consolidated_invoice"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2021-10-21 20:19:38.667508",
|
"modified": "2021-12-23 20:19:38.667508",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice",
|
"name": "Sales Invoice",
|
||||||
|
@ -121,20 +121,21 @@ class Deferred_Item(object):
|
|||||||
"""
|
"""
|
||||||
simulate future posting by creating dummy gl entries. starts from the last posting date.
|
simulate future posting by creating dummy gl entries. starts from the last posting date.
|
||||||
"""
|
"""
|
||||||
if add_days(self.last_entry_date, 1) < self.period_list[-1].to_date:
|
if self.service_start_date != self.service_end_date:
|
||||||
self.estimate_for_period_list = get_period_list(
|
if add_days(self.last_entry_date, 1) < self.period_list[-1].to_date:
|
||||||
self.filters.from_fiscal_year,
|
self.estimate_for_period_list = get_period_list(
|
||||||
self.filters.to_fiscal_year,
|
self.filters.from_fiscal_year,
|
||||||
add_days(self.last_entry_date, 1),
|
self.filters.to_fiscal_year,
|
||||||
self.period_list[-1].to_date,
|
add_days(self.last_entry_date, 1),
|
||||||
"Date Range",
|
self.period_list[-1].to_date,
|
||||||
"Monthly",
|
"Date Range",
|
||||||
company=self.filters.company,
|
"Monthly",
|
||||||
)
|
company=self.filters.company,
|
||||||
for period in self.estimate_for_period_list:
|
)
|
||||||
amount = self.calculate_amount(period.from_date, period.to_date)
|
for period in self.estimate_for_period_list:
|
||||||
gle = self.make_dummy_gle(period.key, period.to_date, amount)
|
amount = self.calculate_amount(period.from_date, period.to_date)
|
||||||
self.gle_entries.append(gle)
|
gle = self.make_dummy_gle(period.key, period.to_date, amount)
|
||||||
|
self.gle_entries.append(gle)
|
||||||
|
|
||||||
def calculate_item_revenue_expense_for_period(self):
|
def calculate_item_revenue_expense_for_period(self):
|
||||||
"""
|
"""
|
||||||
|
@ -167,7 +167,7 @@ frappe.query_reports["General Ledger"] = {
|
|||||||
"fieldname": "include_dimensions",
|
"fieldname": "include_dimensions",
|
||||||
"label": __("Consider Accounting Dimensions"),
|
"label": __("Consider Accounting Dimensions"),
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"default": 0
|
"default": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "show_opening_entries",
|
"fieldname": "show_opening_entries",
|
||||||
|
@ -448,9 +448,11 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
|||||||
|
|
||||||
elif group_by_voucher_consolidated:
|
elif group_by_voucher_consolidated:
|
||||||
keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")]
|
keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")]
|
||||||
for dim in accounting_dimensions:
|
if filters.get("include_dimensions"):
|
||||||
keylist.append(gle.get(dim))
|
for dim in accounting_dimensions:
|
||||||
keylist.append(gle.get("cost_center"))
|
keylist.append(gle.get(dim))
|
||||||
|
keylist.append(gle.get("cost_center"))
|
||||||
|
|
||||||
key = tuple(keylist)
|
key = tuple(keylist)
|
||||||
if key not in consolidated_gle:
|
if key not in consolidated_gle:
|
||||||
consolidated_gle.setdefault(key, gle)
|
consolidated_gle.setdefault(key, gle)
|
||||||
@ -547,10 +549,7 @@ def get_columns(filters):
|
|||||||
"fieldname": "balance",
|
"fieldname": "balance",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"width": 130
|
"width": 130
|
||||||
}
|
},
|
||||||
]
|
|
||||||
|
|
||||||
columns.extend([
|
|
||||||
{
|
{
|
||||||
"label": _("Voucher Type"),
|
"label": _("Voucher Type"),
|
||||||
"fieldname": "voucher_type",
|
"fieldname": "voucher_type",
|
||||||
@ -584,7 +583,7 @@ def get_columns(filters):
|
|||||||
"fieldname": "project",
|
"fieldname": "project",
|
||||||
"width": 100
|
"width": 100
|
||||||
}
|
}
|
||||||
])
|
]
|
||||||
|
|
||||||
if filters.get("include_dimensions"):
|
if filters.get("include_dimensions"):
|
||||||
for dim in get_accounting_dimensions(as_list = False):
|
for dim in get_accounting_dimensions(as_list = False):
|
||||||
@ -594,14 +593,14 @@ def get_columns(filters):
|
|||||||
"fieldname": dim.fieldname,
|
"fieldname": dim.fieldname,
|
||||||
"width": 100
|
"width": 100
|
||||||
})
|
})
|
||||||
|
columns.append({
|
||||||
columns.extend([
|
|
||||||
{
|
|
||||||
"label": _("Cost Center"),
|
"label": _("Cost Center"),
|
||||||
"options": "Cost Center",
|
"options": "Cost Center",
|
||||||
"fieldname": "cost_center",
|
"fieldname": "cost_center",
|
||||||
"width": 100
|
"width": 100
|
||||||
},
|
})
|
||||||
|
|
||||||
|
columns.extend([
|
||||||
{
|
{
|
||||||
"label": _("Against Voucher Type"),
|
"label": _("Against Voucher Type"),
|
||||||
"fieldname": "against_voucher_type",
|
"fieldname": "against_voucher_type",
|
||||||
|
48
erpnext/accounts/test/test_reports.py
Normal file
48
erpnext/accounts/test/test_reports.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import unittest
|
||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
from erpnext.tests.utils import ReportFilters, ReportName, execute_script_report
|
||||||
|
|
||||||
|
DEFAULT_FILTERS = {
|
||||||
|
"company": "_Test Company",
|
||||||
|
"from_date": "2010-01-01",
|
||||||
|
"to_date": "2030-01-01",
|
||||||
|
"period_start_date": "2010-01-01",
|
||||||
|
"period_end_date": "2030-01-01"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [
|
||||||
|
("General Ledger", {"group_by": "Group by Voucher (Consolidated)"} ),
|
||||||
|
("General Ledger", {"group_by": "Group by Voucher (Consolidated)", "include_dimensions": 1} ),
|
||||||
|
("Accounts Payable", {"range1": 30, "range2": 60, "range3": 90, "range4": 120}),
|
||||||
|
("Accounts Receivable", {"range1": 30, "range2": 60, "range3": 90, "range4": 120}),
|
||||||
|
("Consolidated Financial Statement", {"report": "Balance Sheet"} ),
|
||||||
|
("Consolidated Financial Statement", {"report": "Profit and Loss Statement"} ),
|
||||||
|
("Consolidated Financial Statement", {"report": "Cash Flow"} ),
|
||||||
|
("Gross Profit", {"group_by": "Invoice"}),
|
||||||
|
("Gross Profit", {"group_by": "Item Code"}),
|
||||||
|
("Gross Profit", {"group_by": "Item Group"}),
|
||||||
|
("Gross Profit", {"group_by": "Customer"}),
|
||||||
|
("Gross Profit", {"group_by": "Customer Group"}),
|
||||||
|
("Item-wise Sales Register", {}),
|
||||||
|
("Item-wise Purchase Register", {}),
|
||||||
|
("Sales Register", {}),
|
||||||
|
("Purchase Register", {}),
|
||||||
|
("Tax Detail", {"mode": "run", "report_name": "Tax Detail"},),
|
||||||
|
]
|
||||||
|
|
||||||
|
OPTIONAL_FILTERS = {}
|
||||||
|
|
||||||
|
|
||||||
|
class TestReports(unittest.TestCase):
|
||||||
|
def test_execute_all_accounts_reports(self):
|
||||||
|
"""Test that all script report in stock modules are executable with supported filters"""
|
||||||
|
for report, filter in REPORT_FILTER_TEST_CASES:
|
||||||
|
execute_script_report(
|
||||||
|
report_name=report,
|
||||||
|
module="Accounts",
|
||||||
|
filters=filter,
|
||||||
|
default_filters=DEFAULT_FILTERS,
|
||||||
|
optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
|
||||||
|
)
|
@ -608,7 +608,17 @@ class Asset(AccountsController):
|
|||||||
return purchase_document
|
return purchase_document
|
||||||
|
|
||||||
def get_fixed_asset_account(self):
|
def get_fixed_asset_account(self):
|
||||||
return get_asset_category_account('fixed_asset_account', None, self.name, None, self.asset_category, self.company)
|
fixed_asset_account = get_asset_category_account('fixed_asset_account', None, self.name, None, self.asset_category, self.company)
|
||||||
|
if not fixed_asset_account:
|
||||||
|
frappe.throw(
|
||||||
|
_("Set {0} in asset category {1} for company {2}").format(
|
||||||
|
frappe.bold("Fixed Asset Account"),
|
||||||
|
frappe.bold(self.asset_category),
|
||||||
|
frappe.bold(self.company),
|
||||||
|
),
|
||||||
|
title=_("Account not Found"),
|
||||||
|
)
|
||||||
|
return fixed_asset_account
|
||||||
|
|
||||||
def get_cwip_account(self, cwip_enabled=False):
|
def get_cwip_account(self, cwip_enabled=False):
|
||||||
cwip_account = None
|
cwip_account = None
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import unittest
|
import unittest
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
|
||||||
from erpnext.controllers import queries
|
from erpnext.controllers import queries
|
||||||
|
|
||||||
|
|
||||||
@ -85,3 +87,6 @@ class TestQueries(unittest.TestCase):
|
|||||||
|
|
||||||
wh = query(filters=[["Bin", "item_code", "=", "_Test Item"]])
|
wh = query(filters=[["Bin", "item_code", "=", "_Test Item"]])
|
||||||
self.assertGreaterEqual(len(wh), 1)
|
self.assertGreaterEqual(len(wh), 1)
|
||||||
|
|
||||||
|
def test_default_uoms(self):
|
||||||
|
self.assertGreaterEqual(frappe.db.count("UOM", {"enabled": 1}), 10)
|
||||||
|
@ -4,19 +4,72 @@ import frappe
|
|||||||
|
|
||||||
|
|
||||||
class TestUtils(unittest.TestCase):
|
class TestUtils(unittest.TestCase):
|
||||||
def test_reset_default_field_value(self):
|
def test_reset_default_field_value(self):
|
||||||
doc = frappe.get_doc({
|
doc = frappe.get_doc({
|
||||||
"doctype": "Purchase Receipt",
|
"doctype": "Purchase Receipt",
|
||||||
"set_warehouse": "Warehouse 1",
|
"set_warehouse": "Warehouse 1",
|
||||||
})
|
})
|
||||||
|
|
||||||
# Same values
|
# Same values
|
||||||
doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}]
|
doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}]
|
||||||
doc.reset_default_field_value("set_warehouse", "items", "warehouse")
|
doc.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||||
self.assertEqual(doc.set_warehouse, "Warehouse 1")
|
self.assertEqual(doc.set_warehouse, "Warehouse 1")
|
||||||
|
|
||||||
# Mixed values
|
# Mixed values
|
||||||
doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 2"}, {"warehouse": "Warehouse 1"}]
|
doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 2"}, {"warehouse": "Warehouse 1"}]
|
||||||
doc.reset_default_field_value("set_warehouse", "items", "warehouse")
|
doc.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||||
self.assertEqual(doc.set_warehouse, None)
|
self.assertEqual(doc.set_warehouse, None)
|
||||||
|
|
||||||
|
def test_reset_default_field_value_in_mfg_stock_entry(self):
|
||||||
|
# manufacture stock entry with rows having blank source/target wh
|
||||||
|
se = frappe.get_doc(
|
||||||
|
doctype="Stock Entry",
|
||||||
|
purpose="Manufacture",
|
||||||
|
stock_entry_type="Manufacture",
|
||||||
|
company="_Test Company",
|
||||||
|
from_warehouse="_Test Warehouse - _TC",
|
||||||
|
to_warehouse="_Test Warehouse 1 - _TC",
|
||||||
|
items=[
|
||||||
|
frappe._dict(item_code="_Test Item", qty=1, basic_rate=200, s_warehouse="_Test Warehouse - _TC"),
|
||||||
|
frappe._dict(item_code="_Test FG Item", qty=4, t_warehouse="_Test Warehouse 1 - _TC", is_finished_item=1)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
se.save()
|
||||||
|
|
||||||
|
# default fields must be untouched
|
||||||
|
self.assertEqual(se.from_warehouse, "_Test Warehouse - _TC")
|
||||||
|
self.assertEqual(se.to_warehouse, "_Test Warehouse 1 - _TC")
|
||||||
|
|
||||||
|
se.delete()
|
||||||
|
|
||||||
|
def test_reset_default_field_value_in_transfer_stock_entry(self):
|
||||||
|
doc = frappe.get_doc({
|
||||||
|
"doctype": "Stock Entry",
|
||||||
|
"purpose": "Material Receipt",
|
||||||
|
"from_warehouse": "Warehouse 1",
|
||||||
|
"to_warehouse": "Warehouse 2",
|
||||||
|
})
|
||||||
|
|
||||||
|
# Same values
|
||||||
|
doc.items = [
|
||||||
|
{"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"},
|
||||||
|
{"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"},
|
||||||
|
{"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"}
|
||||||
|
]
|
||||||
|
|
||||||
|
doc.reset_default_field_value("from_warehouse", "items", "s_warehouse")
|
||||||
|
doc.reset_default_field_value("to_warehouse", "items", "t_warehouse")
|
||||||
|
self.assertEqual(doc.from_warehouse, "Warehouse 1")
|
||||||
|
self.assertEqual(doc.to_warehouse, "Warehouse 2")
|
||||||
|
|
||||||
|
# Mixed values in source wh
|
||||||
|
doc.items = [
|
||||||
|
{"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"},
|
||||||
|
{"s_warehouse": "Warehouse 3", "t_warehouse": "Warehouse 2"},
|
||||||
|
{"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"}
|
||||||
|
]
|
||||||
|
|
||||||
|
doc.reset_default_field_value("from_warehouse", "items", "s_warehouse")
|
||||||
|
doc.reset_default_field_value("to_warehouse", "items", "t_warehouse")
|
||||||
|
self.assertEqual(doc.from_warehouse, None)
|
||||||
|
self.assertEqual(doc.to_warehouse, "Warehouse 2")
|
@ -201,8 +201,8 @@ def get_course_schedule_events(start, end, filters=None):
|
|||||||
conditions = get_event_conditions("Course Schedule", filters)
|
conditions = get_event_conditions("Course Schedule", filters)
|
||||||
|
|
||||||
data = frappe.db.sql("""select name, course, color,
|
data = frappe.db.sql("""select name, course, color,
|
||||||
timestamp(schedule_date, from_time) as from_datetime,
|
timestamp(schedule_date, from_time) as from_time,
|
||||||
timestamp(schedule_date, to_time) as to_datetime,
|
timestamp(schedule_date, to_time) as to_time,
|
||||||
room, student_group, 0 as 'allDay'
|
room, student_group, 0 as 'allDay'
|
||||||
from `tabCourse Schedule`
|
from `tabCourse Schedule`
|
||||||
where ( schedule_date between %(start)s and %(end)s )
|
where ( schedule_date between %(start)s and %(end)s )
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
@ -30,6 +32,14 @@ class CourseSchedule(Document):
|
|||||||
if self.from_time > self.to_time:
|
if self.from_time > self.to_time:
|
||||||
frappe.throw(_("From Time cannot be greater than To Time."))
|
frappe.throw(_("From Time cannot be greater than To Time."))
|
||||||
|
|
||||||
|
"""Handles specicfic case to update schedule date in calendar """
|
||||||
|
if isinstance(self.from_time, str):
|
||||||
|
try:
|
||||||
|
datetime_obj = datetime.strptime(self.from_time, '%Y-%m-%d %H:%M:%S')
|
||||||
|
self.schedule_date = datetime_obj
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
def validate_overlap(self):
|
def validate_overlap(self):
|
||||||
"""Validates overlap for Student Group, Instructor, Room"""
|
"""Validates overlap for Student Group, Instructor, Room"""
|
||||||
|
|
||||||
@ -47,4 +57,4 @@ class CourseSchedule(Document):
|
|||||||
validate_overlap_for(self, "Assessment Plan", "student_group")
|
validate_overlap_for(self, "Assessment Plan", "student_group")
|
||||||
|
|
||||||
validate_overlap_for(self, "Assessment Plan", "room")
|
validate_overlap_for(self, "Assessment Plan", "room")
|
||||||
validate_overlap_for(self, "Assessment Plan", "supervisor", self.instructor)
|
validate_overlap_for(self, "Assessment Plan", "supervisor", self.instructor)
|
@ -1,11 +1,10 @@
|
|||||||
frappe.views.calendar["Course Schedule"] = {
|
frappe.views.calendar["Course Schedule"] = {
|
||||||
field_map: {
|
field_map: {
|
||||||
// from_datetime and to_datetime don't exist as docfields but are used in onload
|
"start": "from_time",
|
||||||
"start": "from_datetime",
|
"end": "to_time",
|
||||||
"end": "to_datetime",
|
|
||||||
"id": "name",
|
"id": "name",
|
||||||
"title": "course",
|
"title": "course",
|
||||||
"allDay": "allDay"
|
"allDay": "allDay",
|
||||||
},
|
},
|
||||||
gantt: false,
|
gantt: false,
|
||||||
order_by: "schedule_date",
|
order_by: "schedule_date",
|
||||||
|
@ -6,6 +6,7 @@ import unittest
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import to_timedelta, today
|
from frappe.utils import to_timedelta, today
|
||||||
|
from frappe.utils.data import add_to_date
|
||||||
|
|
||||||
from erpnext.education.utils import OverlapError
|
from erpnext.education.utils import OverlapError
|
||||||
|
|
||||||
@ -39,6 +40,11 @@ class TestCourseSchedule(unittest.TestCase):
|
|||||||
make_course_schedule_test_record(from_time= cs1.from_time, to_time= cs1.to_time,
|
make_course_schedule_test_record(from_time= cs1.from_time, to_time= cs1.to_time,
|
||||||
student_group="Course-TC102-2014-2015 (_Test Academic Term)", instructor="_Test Instructor 2", room=frappe.get_all("Room")[1].name)
|
student_group="Course-TC102-2014-2015 (_Test Academic Term)", instructor="_Test Instructor 2", room=frappe.get_all("Room")[1].name)
|
||||||
|
|
||||||
|
def test_update_schedule_date(self):
|
||||||
|
doc = make_course_schedule_test_record(schedule_date= add_to_date(today(), days=1))
|
||||||
|
doc.schedule_date = add_to_date(doc.schedule_date, days=1)
|
||||||
|
doc.save()
|
||||||
|
|
||||||
def make_course_schedule_test_record(**args):
|
def make_course_schedule_test_record(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ class TestEmployeeOnboarding(unittest.TestCase):
|
|||||||
if frappe.db.exists('Employee Onboarding', {'employee_name': 'Test Researcher'}):
|
if frappe.db.exists('Employee Onboarding', {'employee_name': 'Test Researcher'}):
|
||||||
frappe.delete_doc('Employee Onboarding', {'employee_name': 'Test Researcher'})
|
frappe.delete_doc('Employee Onboarding', {'employee_name': 'Test Researcher'})
|
||||||
|
|
||||||
project = "Employee Onboarding : Test Researcher - test@researcher.com"
|
project = "Employee Onboarding : test@researcher.com"
|
||||||
frappe.db.sql("delete from tabProject where name=%s", project)
|
frappe.db.sql("delete from tabProject where name=%s", project)
|
||||||
frappe.db.sql("delete from tabTask where project=%s", project)
|
frappe.db.sql("delete from tabTask where project=%s", project)
|
||||||
|
|
||||||
@ -27,7 +27,7 @@ class TestEmployeeOnboarding(unittest.TestCase):
|
|||||||
onboarding = create_employee_onboarding()
|
onboarding = create_employee_onboarding()
|
||||||
|
|
||||||
project_name = frappe.db.get_value('Project', onboarding.project, 'project_name')
|
project_name = frappe.db.get_value('Project', onboarding.project, 'project_name')
|
||||||
self.assertEqual(project_name, 'Employee Onboarding : Test Researcher - test@researcher.com')
|
self.assertEqual(project_name, 'Employee Onboarding : test@researcher.com')
|
||||||
|
|
||||||
# don't allow making employee if onboarding is not complete
|
# don't allow making employee if onboarding is not complete
|
||||||
self.assertRaises(IncompleteTaskError, make_employee, onboarding.name)
|
self.assertRaises(IncompleteTaskError, make_employee, onboarding.name)
|
||||||
@ -64,8 +64,8 @@ class TestEmployeeOnboarding(unittest.TestCase):
|
|||||||
|
|
||||||
|
|
||||||
def get_job_applicant():
|
def get_job_applicant():
|
||||||
if frappe.db.exists('Job Applicant', 'Test Researcher - test@researcher.com'):
|
if frappe.db.exists('Job Applicant', 'test@researcher.com'):
|
||||||
return frappe.get_doc('Job Applicant', 'Test Researcher - test@researcher.com')
|
return frappe.get_doc('Job Applicant', 'test@researcher.com')
|
||||||
applicant = frappe.new_doc('Job Applicant')
|
applicant = frappe.new_doc('Job Applicant')
|
||||||
applicant.applicant_name = 'Test Researcher'
|
applicant.applicant_name = 'Test Researcher'
|
||||||
applicant.email_id = 'test@researcher.com'
|
applicant.email_id = 'test@researcher.com'
|
||||||
|
@ -192,10 +192,11 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-09-29 23:06:10.904260",
|
"modified": "2022-01-12 16:28:53.196881",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "Job Applicant",
|
"name": "Job Applicant",
|
||||||
|
"naming_rule": "Expression (old style)",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
@ -210,10 +211,11 @@
|
|||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"search_fields": "applicant_name",
|
"search_fields": "applicant_name, email_id, job_title, phone_number",
|
||||||
"sender_field": "email_id",
|
"sender_field": "email_id",
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "ASC",
|
"sort_order": "ASC",
|
||||||
|
"states": [],
|
||||||
"subject_field": "notes",
|
"subject_field": "notes",
|
||||||
"title_field": "applicant_name"
|
"title_field": "applicant_name"
|
||||||
}
|
}
|
@ -7,6 +7,7 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
from frappe.model.naming import append_number_if_name_exists
|
||||||
from frappe.utils import validate_email_address
|
from frappe.utils import validate_email_address
|
||||||
|
|
||||||
from erpnext.hr.doctype.interview.interview import get_interviewers
|
from erpnext.hr.doctype.interview.interview import get_interviewers
|
||||||
@ -21,10 +22,11 @@ class JobApplicant(Document):
|
|||||||
self.get("__onload").job_offer = job_offer[0].name
|
self.get("__onload").job_offer = job_offer[0].name
|
||||||
|
|
||||||
def autoname(self):
|
def autoname(self):
|
||||||
keys = filter(None, (self.applicant_name, self.email_id, self.job_title))
|
self.name = self.email_id
|
||||||
if not keys:
|
|
||||||
frappe.throw(_("Name or Email is mandatory"), frappe.NameError)
|
# applicant can apply more than once for a different job title or reapply
|
||||||
self.name = " - ".join(keys)
|
if frappe.db.exists("Job Applicant", self.name):
|
||||||
|
self.name = append_number_if_name_exists("Job Applicant", self.name)
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
if self.email_id:
|
if self.email_id:
|
||||||
|
@ -9,7 +9,26 @@ from erpnext.hr.doctype.designation.test_designation import create_designation
|
|||||||
|
|
||||||
|
|
||||||
class TestJobApplicant(unittest.TestCase):
|
class TestJobApplicant(unittest.TestCase):
|
||||||
pass
|
def test_job_applicant_naming(self):
|
||||||
|
applicant = frappe.get_doc({
|
||||||
|
"doctype": "Job Applicant",
|
||||||
|
"status": "Open",
|
||||||
|
"applicant_name": "_Test Applicant",
|
||||||
|
"email_id": "job_applicant_naming@example.com"
|
||||||
|
}).insert()
|
||||||
|
self.assertEqual(applicant.name, 'job_applicant_naming@example.com')
|
||||||
|
|
||||||
|
applicant = frappe.get_doc({
|
||||||
|
"doctype": "Job Applicant",
|
||||||
|
"status": "Open",
|
||||||
|
"applicant_name": "_Test Applicant",
|
||||||
|
"email_id": "job_applicant_naming@example.com"
|
||||||
|
}).insert()
|
||||||
|
self.assertEqual(applicant.name, 'job_applicant_naming@example.com-1')
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
frappe.db.rollback()
|
||||||
|
|
||||||
|
|
||||||
def create_job_applicant(**args):
|
def create_job_applicant(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
@ -1,294 +1,108 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"actions": [],
|
||||||
"allow_guest_to_view": 0,
|
|
||||||
"allow_import": 1,
|
"allow_import": 1,
|
||||||
"allow_rename": 1,
|
"allow_rename": 1,
|
||||||
"autoname": "HR-LPR-.YYYY.-.#####",
|
"autoname": "HR-LPR-.YYYY.-.#####",
|
||||||
"beta": 0,
|
|
||||||
"creation": "2018-04-13 15:20:52.864288",
|
"creation": "2018-04-13 15:20:52.864288",
|
||||||
"custom": 0,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"document_type": "",
|
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"from_date",
|
||||||
|
"to_date",
|
||||||
|
"is_active",
|
||||||
|
"column_break_3",
|
||||||
|
"company",
|
||||||
|
"optional_holiday_list"
|
||||||
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "from_date",
|
"fieldname": "from_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "From Date",
|
"label": "From Date",
|
||||||
"length": 0,
|
"reqd": 1
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "to_date",
|
"fieldname": "to_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "To Date",
|
"label": "To Date",
|
||||||
"length": 0,
|
"reqd": 1
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"default": "0",
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "is_active",
|
"fieldname": "is_active",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"hidden": 0,
|
"label": "Is Active"
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Is Active",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "column_break_3",
|
"fieldname": "column_break_3",
|
||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break"
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "company",
|
"fieldname": "company",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Company",
|
"label": "Company",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Company",
|
"options": "Company",
|
||||||
"permlevel": 0,
|
"reqd": 1
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "optional_holiday_list",
|
"fieldname": "optional_holiday_list",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Holiday List for Optional Leave",
|
"label": "Holiday List for Optional Leave",
|
||||||
"length": 0,
|
"options": "Holiday List"
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Holiday List",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"has_web_view": 0,
|
"links": [],
|
||||||
"hide_heading": 0,
|
"modified": "2022-01-13 13:28:12.951025",
|
||||||
"hide_toolbar": 0,
|
|
||||||
"idx": 0,
|
|
||||||
"image_view": 0,
|
|
||||||
"in_create": 0,
|
|
||||||
"is_submittable": 0,
|
|
||||||
"issingle": 0,
|
|
||||||
"istable": 0,
|
|
||||||
"max_attachments": 0,
|
|
||||||
"modified": "2019-05-30 16:15:43.305502",
|
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "Leave Period",
|
"name": "Leave Period",
|
||||||
"name_case": "",
|
"naming_rule": "Expression (old style)",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
"amend": 0,
|
|
||||||
"cancel": 0,
|
|
||||||
"create": 1,
|
"create": 1,
|
||||||
"delete": 1,
|
"delete": 1,
|
||||||
"email": 1,
|
"email": 1,
|
||||||
"export": 1,
|
"export": 1,
|
||||||
"if_owner": 0,
|
|
||||||
"import": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "System Manager",
|
"role": "System Manager",
|
||||||
"set_user_permissions": 0,
|
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"submit": 0,
|
|
||||||
"write": 1
|
"write": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amend": 0,
|
|
||||||
"cancel": 0,
|
|
||||||
"create": 1,
|
"create": 1,
|
||||||
"delete": 1,
|
"delete": 1,
|
||||||
"email": 1,
|
"email": 1,
|
||||||
"export": 1,
|
"export": 1,
|
||||||
"if_owner": 0,
|
|
||||||
"import": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "HR Manager",
|
"role": "HR Manager",
|
||||||
"set_user_permissions": 0,
|
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"submit": 0,
|
|
||||||
"write": 1
|
"write": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amend": 0,
|
|
||||||
"cancel": 0,
|
|
||||||
"create": 1,
|
"create": 1,
|
||||||
"delete": 1,
|
"delete": 1,
|
||||||
"email": 1,
|
"email": 1,
|
||||||
"export": 1,
|
"export": 1,
|
||||||
"if_owner": 0,
|
|
||||||
"import": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "HR User",
|
"role": "HR User",
|
||||||
"set_user_permissions": 0,
|
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"submit": 0,
|
|
||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"quick_entry": 0,
|
"search_fields": "from_date, to_date, company",
|
||||||
"read_only": 0,
|
|
||||||
"read_only_onload": 0,
|
|
||||||
"show_name_in_global_search": 0,
|
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"track_changes": 1,
|
"states": [],
|
||||||
"track_seen": 0,
|
"track_changes": 1
|
||||||
"track_views": 0
|
|
||||||
}
|
}
|
@ -113,10 +113,11 @@
|
|||||||
],
|
],
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-03-01 17:54:01.014509",
|
"modified": "2022-01-13 13:37:11.218882",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "Leave Policy Assignment",
|
"name": "Leave Policy Assignment",
|
||||||
|
"naming_rule": "Expression (old style)",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
@ -164,5 +165,7 @@
|
|||||||
],
|
],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
|
"title_field": "employee_name",
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
@ -48,7 +48,16 @@ frappe.listview_settings['Leave Policy Assignment'] = {
|
|||||||
if (cur_dialog.fields_dict.leave_period.value) {
|
if (cur_dialog.fields_dict.leave_period.value) {
|
||||||
me.set_effective_date();
|
me.set_effective_date();
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
get_query() {
|
||||||
|
let filters = {"is_active": 1};
|
||||||
|
if (cur_dialog.fields_dict.company.value)
|
||||||
|
filters["company"] = cur_dialog.fields_dict.company.value;
|
||||||
|
|
||||||
|
return {
|
||||||
|
filters: filters
|
||||||
|
};
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldtype: "Column Break"
|
fieldtype: "Column Break"
|
||||||
|
@ -530,16 +530,6 @@ class BOM(WebsiteGenerator):
|
|||||||
row.hour_rate = (hour_rate / flt(self.conversion_rate)
|
row.hour_rate = (hour_rate / flt(self.conversion_rate)
|
||||||
if self.conversion_rate and hour_rate else hour_rate)
|
if self.conversion_rate and hour_rate else hour_rate)
|
||||||
|
|
||||||
if self.routing:
|
|
||||||
time_in_mins = flt(frappe.db.get_value("BOM Operation", {
|
|
||||||
"workstation": row.workstation,
|
|
||||||
"operation": row.operation,
|
|
||||||
"parent": self.routing
|
|
||||||
}, ["time_in_mins"]))
|
|
||||||
|
|
||||||
if time_in_mins:
|
|
||||||
row.time_in_mins = time_in_mins
|
|
||||||
|
|
||||||
if row.hour_rate and row.time_in_mins:
|
if row.hour_rate and row.time_in_mins:
|
||||||
row.base_hour_rate = flt(row.hour_rate) * flt(self.conversion_rate)
|
row.base_hour_rate = flt(row.hour_rate) * flt(self.conversion_rate)
|
||||||
row.operating_cost = flt(row.hour_rate) * flt(row.time_in_mins) / 60.0
|
row.operating_cost = flt(row.hour_rate) * flt(row.time_in_mins) / 60.0
|
||||||
|
@ -46,6 +46,7 @@ class TestRouting(ERPNextTestCase):
|
|||||||
wo_doc.delete()
|
wo_doc.delete()
|
||||||
|
|
||||||
def test_update_bom_operation_time(self):
|
def test_update_bom_operation_time(self):
|
||||||
|
"""Update cost shouldn't update routing times."""
|
||||||
operations = [
|
operations = [
|
||||||
{
|
{
|
||||||
"operation": "Test Operation A",
|
"operation": "Test Operation A",
|
||||||
@ -85,8 +86,8 @@ class TestRouting(ERPNextTestCase):
|
|||||||
routing_doc.save()
|
routing_doc.save()
|
||||||
bom_doc.update_cost()
|
bom_doc.update_cost()
|
||||||
bom_doc.reload()
|
bom_doc.reload()
|
||||||
self.assertEqual(bom_doc.operations[0].time_in_mins, 90)
|
self.assertEqual(bom_doc.operations[0].time_in_mins, 30)
|
||||||
self.assertEqual(bom_doc.operations[1].time_in_mins, 42.2)
|
self.assertEqual(bom_doc.operations[1].time_in_mins, 20)
|
||||||
|
|
||||||
|
|
||||||
def setup_operations(rows):
|
def setup_operations(rows):
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import add_months, cint, flt, now, today
|
from frappe.utils import add_days, add_months, cint, flt, now, today
|
||||||
|
|
||||||
from erpnext.manufacturing.doctype.job_card.job_card import JobCardCancelError
|
from erpnext.manufacturing.doctype.job_card.job_card import JobCardCancelError
|
||||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||||
@ -12,6 +12,7 @@ from erpnext.manufacturing.doctype.work_order.work_order import (
|
|||||||
OverProductionError,
|
OverProductionError,
|
||||||
StockOverProductionError,
|
StockOverProductionError,
|
||||||
close_work_order,
|
close_work_order,
|
||||||
|
make_job_card,
|
||||||
make_stock_entry,
|
make_stock_entry,
|
||||||
stop_unstop,
|
stop_unstop,
|
||||||
)
|
)
|
||||||
@ -804,6 +805,34 @@ class TestWorkOrder(ERPNextTestCase):
|
|||||||
if row.is_scrap_item:
|
if row.is_scrap_item:
|
||||||
self.assertEqual(row.qty, 1)
|
self.assertEqual(row.qty, 1)
|
||||||
|
|
||||||
|
# Partial Job Card 1 with qty 10
|
||||||
|
wo_order = make_wo_order_test_record(item=item, company=company, planned_start_date=add_days(now(), 60), qty=20, skip_transfer=1)
|
||||||
|
job_card = frappe.db.get_value('Job Card', {'work_order': wo_order.name}, 'name')
|
||||||
|
update_job_card(job_card, 10)
|
||||||
|
|
||||||
|
stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10))
|
||||||
|
for row in stock_entry.items:
|
||||||
|
if row.is_scrap_item:
|
||||||
|
self.assertEqual(row.qty, 2)
|
||||||
|
|
||||||
|
# Partial Job Card 2 with qty 10
|
||||||
|
operations = []
|
||||||
|
wo_order.load_from_db()
|
||||||
|
for row in wo_order.operations:
|
||||||
|
n_dict = row.as_dict()
|
||||||
|
n_dict['qty'] = 10
|
||||||
|
n_dict['pending_qty'] = 10
|
||||||
|
operations.append(n_dict)
|
||||||
|
|
||||||
|
make_job_card(wo_order.name, operations)
|
||||||
|
job_card = frappe.db.get_value('Job Card', {'work_order': wo_order.name, 'docstatus': 0}, 'name')
|
||||||
|
update_job_card(job_card, 10)
|
||||||
|
|
||||||
|
stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10))
|
||||||
|
for row in stock_entry.items:
|
||||||
|
if row.is_scrap_item:
|
||||||
|
self.assertEqual(row.qty, 2)
|
||||||
|
|
||||||
def test_close_work_order(self):
|
def test_close_work_order(self):
|
||||||
items = ['Test FG Item for Closed WO', 'Test RM Item 1 for Closed WO',
|
items = ['Test FG Item for Closed WO', 'Test RM Item 1 for Closed WO',
|
||||||
'Test RM Item 2 for Closed WO']
|
'Test RM Item 2 for Closed WO']
|
||||||
@ -883,7 +912,8 @@ class TestWorkOrder(ERPNextTestCase):
|
|||||||
self.assertEqual(wo1.operations[0].time_in_mins, wo2.operations[0].time_in_mins)
|
self.assertEqual(wo1.operations[0].time_in_mins, wo2.operations[0].time_in_mins)
|
||||||
|
|
||||||
|
|
||||||
def update_job_card(job_card):
|
def update_job_card(job_card, jc_qty=None):
|
||||||
|
employee = frappe.db.get_value('Employee', {'status': 'Active'}, 'name')
|
||||||
job_card_doc = frappe.get_doc('Job Card', job_card)
|
job_card_doc = frappe.get_doc('Job Card', job_card)
|
||||||
job_card_doc.set('scrap_items', [
|
job_card_doc.set('scrap_items', [
|
||||||
{
|
{
|
||||||
@ -896,8 +926,12 @@ def update_job_card(job_card):
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
|
if jc_qty:
|
||||||
|
job_card_doc.for_quantity = jc_qty
|
||||||
|
|
||||||
job_card_doc.append('time_logs', {
|
job_card_doc.append('time_logs', {
|
||||||
'from_time': now(),
|
'from_time': now(),
|
||||||
|
'employee': employee,
|
||||||
'time_in_mins': 60,
|
'time_in_mins': 60,
|
||||||
'completed_qty': job_card_doc.for_quantity
|
'completed_qty': job_card_doc.for_quantity
|
||||||
})
|
})
|
||||||
|
@ -279,6 +279,7 @@ erpnext.patches.v13_0.add_custom_field_for_south_africa #2
|
|||||||
erpnext.patches.v13_0.update_recipient_email_digest
|
erpnext.patches.v13_0.update_recipient_email_digest
|
||||||
erpnext.patches.v13_0.shopify_deprecation_warning
|
erpnext.patches.v13_0.shopify_deprecation_warning
|
||||||
erpnext.patches.v13_0.remove_bad_selling_defaults
|
erpnext.patches.v13_0.remove_bad_selling_defaults
|
||||||
|
erpnext.patches.v13_0.trim_whitespace_from_serial_nos
|
||||||
erpnext.patches.v13_0.migrate_stripe_api
|
erpnext.patches.v13_0.migrate_stripe_api
|
||||||
erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries
|
erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries
|
||||||
erpnext.patches.v13_0.einvoicing_deprecation_warning
|
erpnext.patches.v13_0.einvoicing_deprecation_warning
|
||||||
@ -304,6 +305,7 @@ erpnext.patches.v13_0.add_default_interview_notification_templates
|
|||||||
erpnext.patches.v13_0.enable_scheduler_job_for_item_reposting
|
erpnext.patches.v13_0.enable_scheduler_job_for_item_reposting
|
||||||
erpnext.patches.v13_0.requeue_failed_reposts
|
erpnext.patches.v13_0.requeue_failed_reposts
|
||||||
erpnext.patches.v13_0.update_job_card_status
|
erpnext.patches.v13_0.update_job_card_status
|
||||||
|
erpnext.patches.v13_0.enable_uoms
|
||||||
erpnext.patches.v12_0.update_production_plan_status
|
erpnext.patches.v12_0.update_production_plan_status
|
||||||
erpnext.patches.v13_0.healthcare_deprecation_warning
|
erpnext.patches.v13_0.healthcare_deprecation_warning
|
||||||
erpnext.patches.v13_0.item_naming_series_not_mandatory
|
erpnext.patches.v13_0.item_naming_series_not_mandatory
|
||||||
@ -322,3 +324,4 @@ execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings'
|
|||||||
erpnext.patches.v14_0.set_payroll_cost_centers
|
erpnext.patches.v14_0.set_payroll_cost_centers
|
||||||
erpnext.patches.v13_0.agriculture_deprecation_warning
|
erpnext.patches.v13_0.agriculture_deprecation_warning
|
||||||
erpnext.patches.v14_0.delete_agriculture_doctypes
|
erpnext.patches.v14_0.delete_agriculture_doctypes
|
||||||
|
erpnext.patches.v13_0.update_exchange_rate_settings
|
||||||
|
13
erpnext/patches/v13_0/enable_uoms.py
Normal file
13
erpnext/patches/v13_0/enable_uoms.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
frappe.reload_doc('setup', 'doctype', 'uom')
|
||||||
|
|
||||||
|
uom = frappe.qb.DocType("UOM")
|
||||||
|
|
||||||
|
(frappe.qb
|
||||||
|
.update(uom)
|
||||||
|
.set(uom.enabled, 1)
|
||||||
|
.where(uom.creation >= "2021-10-18") # date when this field was released
|
||||||
|
).run()
|
65
erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py
Normal file
65
erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
broken_sles = frappe.db.sql("""
|
||||||
|
select name, serial_no
|
||||||
|
from `tabStock Ledger Entry`
|
||||||
|
where
|
||||||
|
is_cancelled = 0
|
||||||
|
and (serial_no like %s or serial_no like %s or serial_no like %s or serial_no like %s)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
" %", # leading whitespace
|
||||||
|
"% ", # trailing whitespace
|
||||||
|
"%\n %", # leading whitespace on newline
|
||||||
|
"% \n%", # trailing whitespace on newline
|
||||||
|
),
|
||||||
|
as_dict=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
frappe.db.MAX_WRITES_PER_TRANSACTION += len(broken_sles)
|
||||||
|
|
||||||
|
if not broken_sles:
|
||||||
|
return
|
||||||
|
|
||||||
|
broken_serial_nos = set()
|
||||||
|
|
||||||
|
# patch SLEs
|
||||||
|
for sle in broken_sles:
|
||||||
|
serial_no_list = get_serial_nos(sle.serial_no)
|
||||||
|
correct_sr_no = "\n".join(serial_no_list)
|
||||||
|
|
||||||
|
if correct_sr_no == sle.serial_no:
|
||||||
|
continue
|
||||||
|
|
||||||
|
frappe.db.set_value("Stock Ledger Entry", sle.name, "serial_no", correct_sr_no, update_modified=False)
|
||||||
|
broken_serial_nos.update(serial_no_list)
|
||||||
|
|
||||||
|
if not broken_serial_nos:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Patch serial No documents if they don't have purchase info
|
||||||
|
# Purchase info is used for fetching incoming rate
|
||||||
|
broken_sr_no_records = frappe.get_list("Serial No",
|
||||||
|
filters={
|
||||||
|
"status":"Active",
|
||||||
|
"name": ("in", broken_serial_nos),
|
||||||
|
"purchase_document_type": ("is", "not set")
|
||||||
|
},
|
||||||
|
pluck="name",
|
||||||
|
)
|
||||||
|
|
||||||
|
frappe.db.MAX_WRITES_PER_TRANSACTION += len(broken_sr_no_records)
|
||||||
|
|
||||||
|
patch_savepoint = "serial_no_patch"
|
||||||
|
for serial_no in broken_sr_no_records:
|
||||||
|
try:
|
||||||
|
frappe.db.savepoint(patch_savepoint)
|
||||||
|
sn = frappe.get_doc("Serial No", serial_no)
|
||||||
|
sn.update_serial_no_reference()
|
||||||
|
sn.db_update()
|
||||||
|
except Exception:
|
||||||
|
frappe.db.rollback(save_point=patch_savepoint)
|
10
erpnext/patches/v13_0/update_exchange_rate_settings.py
Normal file
10
erpnext/patches/v13_0/update_exchange_rate_settings.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
from erpnext.setup.install import setup_currency_exchange
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
frappe.reload_doc("accounts", "doctype", "currency_exchange_settings")
|
||||||
|
frappe.reload_doc("accounts", "doctype", "currency_exchange_settings_result")
|
||||||
|
frappe.reload_doc("accounts", "doctype", "currency_exchange_settings_details")
|
||||||
|
setup_currency_exchange()
|
@ -61,6 +61,8 @@ class PayrollEntry(Document):
|
|||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
frappe.delete_doc("Salary Slip", frappe.db.sql_list("""select name from `tabSalary Slip`
|
frappe.delete_doc("Salary Slip", frappe.db.sql_list("""select name from `tabSalary Slip`
|
||||||
where payroll_entry=%s """, (self.name)))
|
where payroll_entry=%s """, (self.name)))
|
||||||
|
self.db_set("salary_slips_created", 0)
|
||||||
|
self.db_set("salary_slips_submitted", 0)
|
||||||
|
|
||||||
def get_emp_list(self):
|
def get_emp_list(self):
|
||||||
"""
|
"""
|
||||||
|
@ -102,7 +102,7 @@ class Task(NestedSet):
|
|||||||
frappe.throw(_("Completed On cannot be greater than Today"))
|
frappe.throw(_("Completed On cannot be greater than Today"))
|
||||||
|
|
||||||
def update_depends_on(self):
|
def update_depends_on(self):
|
||||||
depends_on_tasks = self.depends_on_tasks or ""
|
depends_on_tasks = ""
|
||||||
for d in self.depends_on:
|
for d in self.depends_on:
|
||||||
if d.task and d.task not in depends_on_tasks:
|
if d.task and d.task not in depends_on_tasks:
|
||||||
depends_on_tasks += d.task + ","
|
depends_on_tasks += d.task + ","
|
||||||
|
@ -68,7 +68,8 @@ def get_data(conditions, filters):
|
|||||||
(soi.billed_amt * IFNULL(so.conversion_rate, 1)) as billed_amount,
|
(soi.billed_amt * IFNULL(so.conversion_rate, 1)) as billed_amount,
|
||||||
(soi.base_amount - (soi.billed_amt * IFNULL(so.conversion_rate, 1))) as pending_amount,
|
(soi.base_amount - (soi.billed_amt * IFNULL(so.conversion_rate, 1))) as pending_amount,
|
||||||
soi.warehouse as warehouse,
|
soi.warehouse as warehouse,
|
||||||
so.company, soi.name
|
so.company, soi.name,
|
||||||
|
soi.description as description
|
||||||
FROM
|
FROM
|
||||||
`tabSales Order` so,
|
`tabSales Order` so,
|
||||||
(`tabSales Order Item` soi
|
(`tabSales Order Item` soi
|
||||||
@ -184,6 +185,12 @@ def get_columns(filters):
|
|||||||
"options": "Item",
|
"options": "Item",
|
||||||
"width": 100
|
"width": 100
|
||||||
})
|
})
|
||||||
|
columns.append({
|
||||||
|
"label":_("Description"),
|
||||||
|
"fieldname": "description",
|
||||||
|
"fieldtype": "Small Text",
|
||||||
|
"width": 100
|
||||||
|
})
|
||||||
|
|
||||||
columns.extend([
|
columns.extend([
|
||||||
{
|
{
|
||||||
|
@ -213,6 +213,9 @@ erpnext.company.setup_queries = function(frm) {
|
|||||||
["default_payroll_payable_account", {"root_type": "Liability"}],
|
["default_payroll_payable_account", {"root_type": "Liability"}],
|
||||||
["round_off_account", {"root_type": "Expense"}],
|
["round_off_account", {"root_type": "Expense"}],
|
||||||
["write_off_account", {"root_type": "Expense"}],
|
["write_off_account", {"root_type": "Expense"}],
|
||||||
|
["default_deferred_expense_account", {}],
|
||||||
|
["default_deferred_revenue_account", {}],
|
||||||
|
["default_expense_claim_payable_account", {}],
|
||||||
["default_discount_account", {}],
|
["default_discount_account", {}],
|
||||||
["discount_allowed_account", {"root_type": "Expense"}],
|
["discount_allowed_account", {"root_type": "Expense"}],
|
||||||
["discount_received_account", {"root_type": "Income"}],
|
["discount_received_account", {"root_type": "Income"}],
|
||||||
|
@ -62,8 +62,13 @@ def patched_requests_get(*args, **kwargs):
|
|||||||
if kwargs['params'].get('date') and kwargs['params'].get('from') and kwargs['params'].get('to'):
|
if kwargs['params'].get('date') and kwargs['params'].get('from') and kwargs['params'].get('to'):
|
||||||
if test_exchange_values.get(kwargs['params']['date']):
|
if test_exchange_values.get(kwargs['params']['date']):
|
||||||
return PatchResponse({'result': test_exchange_values[kwargs['params']['date']]}, 200)
|
return PatchResponse({'result': test_exchange_values[kwargs['params']['date']]}, 200)
|
||||||
|
elif args[0].startswith("https://frankfurter.app") and kwargs.get('params'):
|
||||||
|
if kwargs['params'].get('base') and kwargs['params'].get('symbols'):
|
||||||
|
date = args[0].replace("https://frankfurter.app/", "")
|
||||||
|
if test_exchange_values.get(date):
|
||||||
|
return PatchResponse({'rates': {kwargs['params'].get('symbols'): test_exchange_values.get(date)}}, 200)
|
||||||
|
|
||||||
return PatchResponse({'result': None}, 404)
|
return PatchResponse({'rates': None}, 404)
|
||||||
|
|
||||||
@mock.patch('requests.get', side_effect=patched_requests_get)
|
@mock.patch('requests.get', side_effect=patched_requests_get)
|
||||||
class TestCurrencyExchange(unittest.TestCase):
|
class TestCurrencyExchange(unittest.TestCase):
|
||||||
@ -102,6 +107,41 @@ class TestCurrencyExchange(unittest.TestCase):
|
|||||||
self.assertFalse(exchange_rate == 60)
|
self.assertFalse(exchange_rate == 60)
|
||||||
self.assertEqual(flt(exchange_rate, 3), 65.1)
|
self.assertEqual(flt(exchange_rate, 3), 65.1)
|
||||||
|
|
||||||
|
def test_exchange_rate_via_exchangerate_host(self, mock_get):
|
||||||
|
save_new_records(test_records)
|
||||||
|
|
||||||
|
# Update Currency Exchange Rate
|
||||||
|
settings = frappe.get_single("Currency Exchange Settings")
|
||||||
|
settings.service_provider = 'exchangerate.host'
|
||||||
|
settings.save()
|
||||||
|
|
||||||
|
# Update exchange
|
||||||
|
frappe.db.set_value("Accounts Settings", None, "allow_stale", 1)
|
||||||
|
|
||||||
|
# Start with allow_stale is True
|
||||||
|
exchange_rate = get_exchange_rate("USD", "INR", "2016-01-01", "for_buying")
|
||||||
|
self.assertEqual(flt(exchange_rate, 3), 60.0)
|
||||||
|
|
||||||
|
exchange_rate = get_exchange_rate("USD", "INR", "2016-01-15", "for_buying")
|
||||||
|
self.assertEqual(exchange_rate, 65.1)
|
||||||
|
|
||||||
|
exchange_rate = get_exchange_rate("USD", "INR", "2016-01-30", "for_selling")
|
||||||
|
self.assertEqual(exchange_rate, 62.9)
|
||||||
|
|
||||||
|
# Exchange rate as on 15th Dec, 2015
|
||||||
|
self.clear_cache()
|
||||||
|
exchange_rate = get_exchange_rate("USD", "INR", "2015-12-15", "for_selling")
|
||||||
|
self.assertFalse(exchange_rate == 60)
|
||||||
|
self.assertEqual(flt(exchange_rate, 3), 66.999)
|
||||||
|
|
||||||
|
exchange_rate = get_exchange_rate("USD", "INR", "2016-01-20", "for_buying")
|
||||||
|
self.assertFalse(exchange_rate == 60)
|
||||||
|
self.assertEqual(flt(exchange_rate, 3), 65.1)
|
||||||
|
|
||||||
|
settings = frappe.get_single("Currency Exchange Settings")
|
||||||
|
settings.service_provider = 'frankfurter.app'
|
||||||
|
settings.save()
|
||||||
|
|
||||||
def test_exchange_rate_strict(self, mock_get):
|
def test_exchange_rate_strict(self, mock_get):
|
||||||
# strict currency settings
|
# strict currency settings
|
||||||
frappe.db.set_value("Accounts Settings", None, "allow_stale", 0)
|
frappe.db.set_value("Accounts Settings", None, "allow_stale", 0)
|
||||||
|
@ -60,6 +60,22 @@ def set_single_defaults():
|
|||||||
|
|
||||||
frappe.db.set_default("date_format", "dd-mm-yyyy")
|
frappe.db.set_default("date_format", "dd-mm-yyyy")
|
||||||
|
|
||||||
|
setup_currency_exchange()
|
||||||
|
|
||||||
|
def setup_currency_exchange():
|
||||||
|
ces = frappe.get_single('Currency Exchange Settings')
|
||||||
|
try:
|
||||||
|
ces.set('result_key', [])
|
||||||
|
ces.set('req_params', [])
|
||||||
|
|
||||||
|
ces.api_endpoint = "https://frankfurter.app/{transaction_date}"
|
||||||
|
ces.append('result_key', {'key': 'rates'})
|
||||||
|
ces.append('result_key', {'key': '{to_currency}'})
|
||||||
|
ces.append('req_params', {'key': 'base', 'value': '{from_currency}'})
|
||||||
|
ces.append('req_params', {'key': 'symbols', 'value': '{to_currency}'})
|
||||||
|
ces.save()
|
||||||
|
except frappe.ValidationError:
|
||||||
|
pass
|
||||||
|
|
||||||
def create_compact_item_print_custom_field():
|
def create_compact_item_print_custom_field():
|
||||||
create_custom_field('Print Settings', {
|
create_custom_field('Print Settings', {
|
||||||
|
@ -353,7 +353,8 @@ def add_uom_data():
|
|||||||
"doctype": "UOM",
|
"doctype": "UOM",
|
||||||
"uom_name": _(d.get("uom_name")),
|
"uom_name": _(d.get("uom_name")),
|
||||||
"name": _(d.get("uom_name")),
|
"name": _(d.get("uom_name")),
|
||||||
"must_be_whole_number": d.get("must_be_whole_number")
|
"must_be_whole_number": d.get("must_be_whole_number"),
|
||||||
|
"enabled": 1,
|
||||||
}).db_insert()
|
}).db_insert()
|
||||||
|
|
||||||
# bootstrap uom conversion factors
|
# bootstrap uom conversion factors
|
||||||
|
@ -100,15 +100,21 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No
|
|||||||
|
|
||||||
if not value:
|
if not value:
|
||||||
import requests
|
import requests
|
||||||
api_url = "https://api.exchangerate.host/convert"
|
settings = frappe.get_cached_doc('Currency Exchange Settings')
|
||||||
response = requests.get(api_url, params={
|
req_params = {
|
||||||
"date": transaction_date,
|
"transaction_date": transaction_date,
|
||||||
"from": from_currency,
|
"from_currency": from_currency,
|
||||||
"to": to_currency
|
"to_currency": to_currency
|
||||||
})
|
}
|
||||||
|
params = {}
|
||||||
|
for row in settings.req_params:
|
||||||
|
params[row.key] = format_ces_api(row.value, req_params)
|
||||||
|
response = requests.get(format_ces_api(settings.api_endpoint, req_params), params=params)
|
||||||
# expire in 6 hours
|
# expire in 6 hours
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
value = response.json()["result"]
|
value = response.json()
|
||||||
|
for res_key in settings.result_key:
|
||||||
|
value = value[format_ces_api(str(res_key.key), req_params)]
|
||||||
cache.setex(name=key, time=21600, value=flt(value))
|
cache.setex(name=key, time=21600, value=flt(value))
|
||||||
return flt(value)
|
return flt(value)
|
||||||
except Exception:
|
except Exception:
|
||||||
@ -116,6 +122,13 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No
|
|||||||
frappe.msgprint(_("Unable to find exchange rate for {0} to {1} for key date {2}. Please create a Currency Exchange record manually").format(from_currency, to_currency, transaction_date))
|
frappe.msgprint(_("Unable to find exchange rate for {0} to {1} for key date {2}. Please create a Currency Exchange record manually").format(from_currency, to_currency, transaction_date))
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
|
def format_ces_api(data, param):
|
||||||
|
return data.format(
|
||||||
|
transaction_date=param.get("transaction_date"),
|
||||||
|
to_currency=param.get("to_currency"),
|
||||||
|
from_currency=param.get("from_currency")
|
||||||
|
)
|
||||||
|
|
||||||
def enable_all_roles_and_domains():
|
def enable_all_roles_and_domains():
|
||||||
""" enable all roles and domain for testing """
|
""" enable all roles and domain for testing """
|
||||||
# add all roles to users
|
# add all roles to users
|
||||||
|
@ -492,18 +492,20 @@ class Item(WebsiteGenerator):
|
|||||||
context.shopping_cart = get_product_info_for_website(self.name, skip_quotation_creation=True)
|
context.shopping_cart = get_product_info_for_website(self.name, skip_quotation_creation=True)
|
||||||
|
|
||||||
def add_default_uom_in_conversion_factor_table(self):
|
def add_default_uom_in_conversion_factor_table(self):
|
||||||
uom_conv_list = [d.uom for d in self.get("uoms")]
|
if not self.is_new() and self.has_value_changed("stock_uom"):
|
||||||
if self.stock_uom not in uom_conv_list:
|
self.uoms = []
|
||||||
ch = self.append('uoms', {})
|
frappe.msgprint(
|
||||||
ch.uom = self.stock_uom
|
_("Successfully changed Stock UOM, please redefine conversion factors for new UOM."),
|
||||||
ch.conversion_factor = 1
|
alert=True,
|
||||||
|
)
|
||||||
|
|
||||||
to_remove = []
|
uoms_list = [d.uom for d in self.get("uoms")]
|
||||||
for d in self.get("uoms"):
|
|
||||||
if d.conversion_factor == 1 and d.uom != self.stock_uom:
|
|
||||||
to_remove.append(d)
|
|
||||||
|
|
||||||
[self.remove(d) for d in to_remove]
|
if self.stock_uom not in uoms_list:
|
||||||
|
self.append("uoms", {
|
||||||
|
"uom": self.stock_uom,
|
||||||
|
"conversion_factor": 1
|
||||||
|
})
|
||||||
|
|
||||||
def update_show_in_website(self):
|
def update_show_in_website(self):
|
||||||
if self.disabled:
|
if self.disabled:
|
||||||
|
@ -584,6 +584,16 @@ class TestItem(ERPNextTestCase):
|
|||||||
except frappe.ValidationError as e:
|
except frappe.ValidationError as e:
|
||||||
self.fail(f"UoM change not allowed even though no SLE / BIN with positive qty exists: {e}")
|
self.fail(f"UoM change not allowed even though no SLE / BIN with positive qty exists: {e}")
|
||||||
|
|
||||||
|
def test_erasure_of_old_conversions(self):
|
||||||
|
item = create_item("_item change uom")
|
||||||
|
item.stock_uom = "Gram"
|
||||||
|
item.append("uoms", frappe._dict(uom="Box", conversion_factor=2))
|
||||||
|
item.save()
|
||||||
|
item.reload()
|
||||||
|
item.stock_uom = "Nos"
|
||||||
|
item.save()
|
||||||
|
self.assertEqual(len(item.uoms), 1)
|
||||||
|
|
||||||
def test_validate_stock_item(self):
|
def test_validate_stock_item(self):
|
||||||
self.assertRaises(frappe.ValidationError, validate_is_stock_item, "_Test Non Stock Item")
|
self.assertRaises(frappe.ValidationError, validate_is_stock_item, "_Test Non Stock Item")
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ from collections import defaultdict
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
|
from frappe.query_builder.functions import Sum
|
||||||
from frappe.utils import cint, comma_or, cstr, flt, format_time, formatdate, getdate, nowdate
|
from frappe.utils import cint, comma_or, cstr, flt, format_time, formatdate, getdate, nowdate
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
@ -85,8 +86,11 @@ class StockEntry(StockController):
|
|||||||
self.validate_warehouse()
|
self.validate_warehouse()
|
||||||
self.validate_work_order()
|
self.validate_work_order()
|
||||||
self.validate_bom()
|
self.validate_bom()
|
||||||
self.mark_finished_and_scrap_items()
|
|
||||||
self.validate_finished_goods()
|
if self.purpose in ("Manufacture", "Repack"):
|
||||||
|
self.mark_finished_and_scrap_items()
|
||||||
|
self.validate_finished_goods()
|
||||||
|
|
||||||
self.validate_with_material_request()
|
self.validate_with_material_request()
|
||||||
self.validate_batch()
|
self.validate_batch()
|
||||||
self.validate_inspection()
|
self.validate_inspection()
|
||||||
@ -109,8 +113,12 @@ class StockEntry(StockController):
|
|||||||
self.set_actual_qty()
|
self.set_actual_qty()
|
||||||
self.calculate_rate_and_amount()
|
self.calculate_rate_and_amount()
|
||||||
self.validate_putaway_capacity()
|
self.validate_putaway_capacity()
|
||||||
self.reset_default_field_value("from_warehouse", "items", "s_warehouse")
|
|
||||||
self.reset_default_field_value("to_warehouse", "items", "t_warehouse")
|
if not self.get("purpose") == "Manufacture":
|
||||||
|
# ignore scrap item wh difference and empty source/target wh
|
||||||
|
# in Manufacture Entry
|
||||||
|
self.reset_default_field_value("from_warehouse", "items", "s_warehouse")
|
||||||
|
self.reset_default_field_value("to_warehouse", "items", "t_warehouse")
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.update_stock_ledger()
|
self.update_stock_ledger()
|
||||||
@ -701,26 +709,25 @@ class StockEntry(StockController):
|
|||||||
validate_bom_no(item_code, d.bom_no)
|
validate_bom_no(item_code, d.bom_no)
|
||||||
|
|
||||||
def mark_finished_and_scrap_items(self):
|
def mark_finished_and_scrap_items(self):
|
||||||
if self.purpose in ("Repack", "Manufacture"):
|
if any([d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)]):
|
||||||
if any([d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)]):
|
return
|
||||||
return
|
|
||||||
|
|
||||||
finished_item = self.get_finished_item()
|
finished_item = self.get_finished_item()
|
||||||
|
|
||||||
if not finished_item and self.purpose == "Manufacture":
|
if not finished_item and self.purpose == "Manufacture":
|
||||||
# In case of independent Manufacture entry, don't auto set
|
# In case of independent Manufacture entry, don't auto set
|
||||||
# user must decide and set
|
# user must decide and set
|
||||||
return
|
return
|
||||||
|
|
||||||
for d in self.items:
|
for d in self.items:
|
||||||
if d.t_warehouse and not d.s_warehouse:
|
if d.t_warehouse and not d.s_warehouse:
|
||||||
if self.purpose=="Repack" or d.item_code == finished_item:
|
if self.purpose=="Repack" or d.item_code == finished_item:
|
||||||
d.is_finished_item = 1
|
d.is_finished_item = 1
|
||||||
else:
|
|
||||||
d.is_scrap_item = 1
|
|
||||||
else:
|
else:
|
||||||
d.is_finished_item = 0
|
d.is_scrap_item = 1
|
||||||
d.is_scrap_item = 0
|
else:
|
||||||
|
d.is_finished_item = 0
|
||||||
|
d.is_scrap_item = 0
|
||||||
|
|
||||||
def get_finished_item(self):
|
def get_finished_item(self):
|
||||||
finished_item = None
|
finished_item = None
|
||||||
@ -733,9 +740,9 @@ class StockEntry(StockController):
|
|||||||
|
|
||||||
def validate_finished_goods(self):
|
def validate_finished_goods(self):
|
||||||
"""
|
"""
|
||||||
1. Check if FG exists
|
1. Check if FG exists (mfg, repack)
|
||||||
2. Check if Multiple FG Items are present
|
2. Check if Multiple FG Items are present (mfg)
|
||||||
3. Check FG Item and Qty against WO if present
|
3. Check FG Item and Qty against WO if present (mfg)
|
||||||
"""
|
"""
|
||||||
production_item, wo_qty, finished_items = None, 0, []
|
production_item, wo_qty, finished_items = None, 0, []
|
||||||
|
|
||||||
@ -748,8 +755,9 @@ class StockEntry(StockController):
|
|||||||
for d in self.get('items'):
|
for d in self.get('items'):
|
||||||
if d.is_finished_item:
|
if d.is_finished_item:
|
||||||
if not self.work_order:
|
if not self.work_order:
|
||||||
|
# Independent MFG Entry/ Repack Entry, no WO to match against
|
||||||
finished_items.append(d.item_code)
|
finished_items.append(d.item_code)
|
||||||
continue # Independent Manufacture Entry, no WO to match against
|
continue
|
||||||
|
|
||||||
if d.item_code != production_item:
|
if d.item_code != production_item:
|
||||||
frappe.throw(_("Finished Item {0} does not match with Work Order {1}")
|
frappe.throw(_("Finished Item {0} does not match with Work Order {1}")
|
||||||
@ -762,19 +770,17 @@ class StockEntry(StockController):
|
|||||||
|
|
||||||
finished_items.append(d.item_code)
|
finished_items.append(d.item_code)
|
||||||
|
|
||||||
if len(set(finished_items)) > 1:
|
if not finished_items:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
msg=_("Multiple items cannot be marked as finished item"),
|
msg=_("There must be atleast 1 Finished Good in this Stock Entry").format(self.name),
|
||||||
title=_("Note"),
|
title=_("Missing Finished Good"), exc=FinishedGoodError
|
||||||
exc=FinishedGoodError
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.purpose == "Manufacture":
|
if self.purpose == "Manufacture":
|
||||||
if not finished_items:
|
if len(set(finished_items)) > 1:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
msg=_("There must be atleast 1 Finished Good in this Stock Entry").format(self.name),
|
msg=_("Multiple items cannot be marked as finished item"),
|
||||||
title=_("Missing Finished Good"),
|
title=_("Note"), exc=FinishedGoodError
|
||||||
exc=FinishedGoodError
|
|
||||||
)
|
)
|
||||||
|
|
||||||
allowance_percentage = flt(
|
allowance_percentage = flt(
|
||||||
@ -1275,22 +1281,29 @@ class StockEntry(StockController):
|
|||||||
if not self.pro_doc:
|
if not self.pro_doc:
|
||||||
self.set_work_order_details()
|
self.set_work_order_details()
|
||||||
|
|
||||||
scrap_items = frappe.db.sql('''
|
if not self.pro_doc.operations:
|
||||||
SELECT
|
|
||||||
JCSI.item_code, JCSI.item_name, SUM(JCSI.stock_qty) as stock_qty, JCSI.stock_uom, JCSI.description
|
|
||||||
FROM
|
|
||||||
`tabJob Card` JC, `tabJob Card Scrap Item` JCSI
|
|
||||||
WHERE
|
|
||||||
JCSI.parent = JC.name AND JC.docstatus = 1
|
|
||||||
AND JCSI.item_code IS NOT NULL AND JC.work_order = %s
|
|
||||||
GROUP BY
|
|
||||||
JCSI.item_code
|
|
||||||
''', self.work_order, as_dict=1)
|
|
||||||
|
|
||||||
pending_qty = flt(self.pro_doc.qty) - flt(self.pro_doc.produced_qty)
|
|
||||||
if pending_qty <=0:
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
job_card = frappe.qb.DocType('Job Card')
|
||||||
|
job_card_scrap_item = frappe.qb.DocType('Job Card Scrap Item')
|
||||||
|
|
||||||
|
scrap_items = (
|
||||||
|
frappe.qb.from_(job_card)
|
||||||
|
.select(
|
||||||
|
Sum(job_card_scrap_item.stock_qty).as_('stock_qty'),
|
||||||
|
job_card_scrap_item.item_code, job_card_scrap_item.item_name,
|
||||||
|
job_card_scrap_item.description, job_card_scrap_item.stock_uom)
|
||||||
|
.join(job_card_scrap_item)
|
||||||
|
.on(job_card_scrap_item.parent == job_card.name)
|
||||||
|
.where(
|
||||||
|
(job_card_scrap_item.item_code.isnotnull())
|
||||||
|
& (job_card.work_order == self.work_order)
|
||||||
|
& (job_card.docstatus == 1))
|
||||||
|
.groupby(job_card_scrap_item.item_code)
|
||||||
|
).run(as_dict=1)
|
||||||
|
|
||||||
|
pending_qty = flt(self.get_completed_job_card_qty()) - flt(self.pro_doc.produced_qty)
|
||||||
|
|
||||||
used_scrap_items = self.get_used_scrap_items()
|
used_scrap_items = self.get_used_scrap_items()
|
||||||
for row in scrap_items:
|
for row in scrap_items:
|
||||||
row.stock_qty -= flt(used_scrap_items.get(row.item_code))
|
row.stock_qty -= flt(used_scrap_items.get(row.item_code))
|
||||||
@ -1304,6 +1317,9 @@ class StockEntry(StockController):
|
|||||||
|
|
||||||
return scrap_items
|
return scrap_items
|
||||||
|
|
||||||
|
def get_completed_job_card_qty(self):
|
||||||
|
return flt(min([d.completed_qty for d in self.pro_doc.operations]))
|
||||||
|
|
||||||
def get_used_scrap_items(self):
|
def get_used_scrap_items(self):
|
||||||
used_scrap_items = defaultdict(float)
|
used_scrap_items = defaultdict(float)
|
||||||
data = frappe.get_all(
|
data = frappe.get_all(
|
||||||
|
@ -226,9 +226,47 @@ class TestStockEntry(ERPNextTestCase):
|
|||||||
|
|
||||||
mtn.cancel()
|
mtn.cancel()
|
||||||
|
|
||||||
def test_repack_no_change_in_valuation(self):
|
def test_repack_multiple_fg(self):
|
||||||
company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company')
|
"Test `is_finished_item` for one item repacked into two items."
|
||||||
|
make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=100, basic_rate=100)
|
||||||
|
|
||||||
|
repack = frappe.copy_doc(test_records[3])
|
||||||
|
repack.posting_date = nowdate()
|
||||||
|
repack.posting_time = nowtime()
|
||||||
|
|
||||||
|
repack.items[0].qty = 100.0
|
||||||
|
repack.items[0].transfer_qty = 100.0
|
||||||
|
repack.items[1].qty = 50.0
|
||||||
|
|
||||||
|
repack.append("items", {
|
||||||
|
"conversion_factor": 1.0,
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
"doctype": "Stock Entry Detail",
|
||||||
|
"expense_account": "Stock Adjustment - _TC",
|
||||||
|
"basic_rate": 150,
|
||||||
|
"item_code": "_Test Item 2",
|
||||||
|
"parentfield": "items",
|
||||||
|
"qty": 50.0,
|
||||||
|
"stock_uom": "_Test UOM",
|
||||||
|
"t_warehouse": "_Test Warehouse - _TC",
|
||||||
|
"transfer_qty": 50.0,
|
||||||
|
"uom": "_Test UOM"
|
||||||
|
})
|
||||||
|
repack.set_stock_entry_type()
|
||||||
|
repack.insert()
|
||||||
|
|
||||||
|
self.assertEqual(repack.items[1].is_finished_item, 1)
|
||||||
|
self.assertEqual(repack.items[2].is_finished_item, 1)
|
||||||
|
|
||||||
|
repack.items[1].is_finished_item = 0
|
||||||
|
repack.items[2].is_finished_item = 0
|
||||||
|
|
||||||
|
# must raise error if 0 fg in repack entry
|
||||||
|
self.assertRaises(FinishedGoodError, repack.validate_finished_goods)
|
||||||
|
|
||||||
|
repack.delete() # teardown
|
||||||
|
|
||||||
|
def test_repack_no_change_in_valuation(self):
|
||||||
make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=50, basic_rate=100)
|
make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=50, basic_rate=100)
|
||||||
make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC",
|
make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC",
|
||||||
qty=50, basic_rate=100)
|
qty=50, basic_rate=100)
|
||||||
|
@ -853,7 +853,7 @@ def get_user_time(user, to_string=False):
|
|||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_sla_doctypes():
|
def get_sla_doctypes():
|
||||||
doctypes = []
|
doctypes = []
|
||||||
data = frappe.get_list('Service Level Agreement',
|
data = frappe.get_all('Service Level Agreement',
|
||||||
{'enabled': 1},
|
{'enabled': 1},
|
||||||
['document_type'],
|
['document_type'],
|
||||||
distinct=1
|
distinct=1
|
||||||
|
@ -125,17 +125,23 @@ def execute_script_report(
|
|||||||
if default_filters is None:
|
if default_filters is None:
|
||||||
default_filters = {}
|
default_filters = {}
|
||||||
|
|
||||||
|
test_filters = []
|
||||||
report_execute_fn = frappe.get_attr(get_report_module_dotted_path(module, report_name) + ".execute")
|
report_execute_fn = frappe.get_attr(get_report_module_dotted_path(module, report_name) + ".execute")
|
||||||
report_filters = frappe._dict(default_filters).copy().update(filters)
|
report_filters = frappe._dict(default_filters).copy().update(filters)
|
||||||
|
|
||||||
report_data = report_execute_fn(report_filters)
|
test_filters.append(report_filters)
|
||||||
|
|
||||||
if optional_filters:
|
if optional_filters:
|
||||||
for key, value in optional_filters.items():
|
for key, value in optional_filters.items():
|
||||||
filter_with_optional_param = report_filters.copy().update({key: value})
|
test_filters.append(report_filters.copy().update({key: value}))
|
||||||
report_execute_fn(filter_with_optional_param)
|
|
||||||
|
for test_filter in test_filters:
|
||||||
|
try:
|
||||||
|
report_execute_fn(test_filter)
|
||||||
|
except Exception:
|
||||||
|
print(f"Report failed to execute with filters: {test_filter}")
|
||||||
|
raise
|
||||||
|
|
||||||
return report_data
|
|
||||||
|
|
||||||
|
|
||||||
def timeout(seconds=30, error_message="Test timed out."):
|
def timeout(seconds=30, error_message="Test timed out."):
|
||||||
|
@ -181,8 +181,6 @@ class TransactionBase(StatusUpdater):
|
|||||||
|
|
||||||
if len(child_table_values) > 1:
|
if len(child_table_values) > 1:
|
||||||
self.set(default_field, None)
|
self.set(default_field, None)
|
||||||
else:
|
|
||||||
self.set(default_field, list(child_table_values)[0])
|
|
||||||
|
|
||||||
def delete_events(ref_type, ref_name):
|
def delete_events(ref_type, ref_name):
|
||||||
events = frappe.db.sql_list(""" SELECT
|
events = frappe.db.sql_list(""" SELECT
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# frappe # https://github.com/frappe/frappe is installed during bench-init
|
# frappe # https://github.com/frappe/frappe is installed during bench-init
|
||||||
gocardless-pro~=1.22.0
|
gocardless-pro~=1.22.0
|
||||||
googlemaps # used in ERPNext, but dependency is defined in Frappe
|
googlemaps
|
||||||
pandas~=1.1.5
|
pandas~=1.1.5
|
||||||
plaid-python~=7.2.1
|
plaid-python~=7.2.1
|
||||||
pycountry~=20.7.3
|
pycountry~=20.7.3
|
||||||
|
Loading…
x
Reference in New Issue
Block a user