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
|
||||
- projects
|
||||
- support
|
||||
- CRM
|
||||
- assets
|
||||
- integrations
|
||||
- quality
|
||||
@ -48,6 +49,7 @@ body:
|
||||
- agriculture
|
||||
- education
|
||||
- non-profit
|
||||
- other
|
||||
validations:
|
||||
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'
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.6
|
||||
python-version: 3.8
|
||||
|
||||
- name: 'Clone repo'
|
||||
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
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.7
|
||||
python-version: 3.8
|
||||
|
||||
- name: Setup Node
|
||||
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
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.7
|
||||
python-version: 3.8
|
||||
|
||||
- name: Setup Node
|
||||
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
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.7
|
||||
python-version: 3.8
|
||||
|
||||
- name: Setup Node
|
||||
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
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.7
|
||||
python-version: 3.8
|
||||
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
|
@ -23,13 +23,13 @@ erpnext/stock/ @marination @rohitwaghchaure @ankush
|
||||
|
||||
erpnext/crm/ @ruchamahabal @pateljannat
|
||||
erpnext/education/ @ruchamahabal @pateljannat
|
||||
erpnext/healthcare/ @ruchamahabal @pateljannat @chillaranand
|
||||
erpnext/hr/ @ruchamahabal @pateljannat
|
||||
erpnext/non_profit/ @ruchamahabal
|
||||
erpnext/payroll @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
|
||||
|
@ -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):
|
||||
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:
|
||||
if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date:
|
||||
if term.discount_type == 'Percentage':
|
||||
|
@ -353,7 +353,6 @@ class POSInvoice(SalesInvoice):
|
||||
if not for_validate and not self.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.set_warehouse = profile.get('warehouse') or self.set_warehouse
|
||||
|
||||
|
@ -556,6 +556,37 @@ class TestPOSInvoice(unittest.TestCase):
|
||||
batch.cancel()
|
||||
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):
|
||||
args = frappe._dict(args)
|
||||
pos_profile = None
|
||||
|
@ -650,7 +650,7 @@ def make_pricing_rule(**args):
|
||||
"rate": args.rate or 0.0,
|
||||
"margin_rate_or_amount": args.margin_rate_or_amount or 0.0,
|
||||
"condition": args.condition or '',
|
||||
"priority": 1,
|
||||
"priority": args.priority or 1,
|
||||
"discount_amount": args.discount_amount or 0.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):
|
||||
doc.db_set(applicable_for, args.get(applicable_for))
|
||||
|
||||
return doc
|
||||
|
||||
def setup_pricing_rule_data():
|
||||
if not frappe.db.exists('Campaign', '_Test Campaign'):
|
||||
frappe.get_doc({
|
||||
|
@ -651,7 +651,7 @@
|
||||
"hide_seconds": 1,
|
||||
"label": "Ignore Pricing Rule",
|
||||
"no_copy": 1,
|
||||
"permlevel": 1,
|
||||
"permlevel": 0,
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
@ -2038,7 +2038,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2021-10-21 20:19:38.667508",
|
||||
"modified": "2021-12-23 20:19:38.667508",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"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.
|
||||
"""
|
||||
if add_days(self.last_entry_date, 1) < self.period_list[-1].to_date:
|
||||
self.estimate_for_period_list = get_period_list(
|
||||
self.filters.from_fiscal_year,
|
||||
self.filters.to_fiscal_year,
|
||||
add_days(self.last_entry_date, 1),
|
||||
self.period_list[-1].to_date,
|
||||
"Date Range",
|
||||
"Monthly",
|
||||
company=self.filters.company,
|
||||
)
|
||||
for period in self.estimate_for_period_list:
|
||||
amount = self.calculate_amount(period.from_date, period.to_date)
|
||||
gle = self.make_dummy_gle(period.key, period.to_date, amount)
|
||||
self.gle_entries.append(gle)
|
||||
if self.service_start_date != self.service_end_date:
|
||||
if add_days(self.last_entry_date, 1) < self.period_list[-1].to_date:
|
||||
self.estimate_for_period_list = get_period_list(
|
||||
self.filters.from_fiscal_year,
|
||||
self.filters.to_fiscal_year,
|
||||
add_days(self.last_entry_date, 1),
|
||||
self.period_list[-1].to_date,
|
||||
"Date Range",
|
||||
"Monthly",
|
||||
company=self.filters.company,
|
||||
)
|
||||
for period in self.estimate_for_period_list:
|
||||
amount = self.calculate_amount(period.from_date, period.to_date)
|
||||
gle = self.make_dummy_gle(period.key, period.to_date, amount)
|
||||
self.gle_entries.append(gle)
|
||||
|
||||
def calculate_item_revenue_expense_for_period(self):
|
||||
"""
|
||||
|
@ -167,7 +167,7 @@ frappe.query_reports["General Ledger"] = {
|
||||
"fieldname": "include_dimensions",
|
||||
"label": __("Consider Accounting Dimensions"),
|
||||
"fieldtype": "Check",
|
||||
"default": 0
|
||||
"default": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "show_opening_entries",
|
||||
|
@ -448,9 +448,11 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
||||
|
||||
elif group_by_voucher_consolidated:
|
||||
keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")]
|
||||
for dim in accounting_dimensions:
|
||||
keylist.append(gle.get(dim))
|
||||
keylist.append(gle.get("cost_center"))
|
||||
if filters.get("include_dimensions"):
|
||||
for dim in accounting_dimensions:
|
||||
keylist.append(gle.get(dim))
|
||||
keylist.append(gle.get("cost_center"))
|
||||
|
||||
key = tuple(keylist)
|
||||
if key not in consolidated_gle:
|
||||
consolidated_gle.setdefault(key, gle)
|
||||
@ -547,10 +549,7 @@ def get_columns(filters):
|
||||
"fieldname": "balance",
|
||||
"fieldtype": "Float",
|
||||
"width": 130
|
||||
}
|
||||
]
|
||||
|
||||
columns.extend([
|
||||
},
|
||||
{
|
||||
"label": _("Voucher Type"),
|
||||
"fieldname": "voucher_type",
|
||||
@ -584,7 +583,7 @@ def get_columns(filters):
|
||||
"fieldname": "project",
|
||||
"width": 100
|
||||
}
|
||||
])
|
||||
]
|
||||
|
||||
if filters.get("include_dimensions"):
|
||||
for dim in get_accounting_dimensions(as_list = False):
|
||||
@ -594,14 +593,14 @@ def get_columns(filters):
|
||||
"fieldname": dim.fieldname,
|
||||
"width": 100
|
||||
})
|
||||
|
||||
columns.extend([
|
||||
{
|
||||
columns.append({
|
||||
"label": _("Cost Center"),
|
||||
"options": "Cost Center",
|
||||
"fieldname": "cost_center",
|
||||
"width": 100
|
||||
},
|
||||
})
|
||||
|
||||
columns.extend([
|
||||
{
|
||||
"label": _("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
|
||||
|
||||
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):
|
||||
cwip_account = None
|
||||
|
@ -1,6 +1,8 @@
|
||||
import unittest
|
||||
from functools import partial
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.controllers import queries
|
||||
|
||||
|
||||
@ -85,3 +87,6 @@ class TestQueries(unittest.TestCase):
|
||||
|
||||
wh = query(filters=[["Bin", "item_code", "=", "_Test Item"]])
|
||||
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):
|
||||
def test_reset_default_field_value(self):
|
||||
doc = frappe.get_doc({
|
||||
"doctype": "Purchase Receipt",
|
||||
"set_warehouse": "Warehouse 1",
|
||||
})
|
||||
def test_reset_default_field_value(self):
|
||||
doc = frappe.get_doc({
|
||||
"doctype": "Purchase Receipt",
|
||||
"set_warehouse": "Warehouse 1",
|
||||
})
|
||||
|
||||
# Same values
|
||||
doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}]
|
||||
doc.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||
self.assertEqual(doc.set_warehouse, "Warehouse 1")
|
||||
# Same values
|
||||
doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}]
|
||||
doc.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||
self.assertEqual(doc.set_warehouse, "Warehouse 1")
|
||||
|
||||
# Mixed values
|
||||
doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 2"}, {"warehouse": "Warehouse 1"}]
|
||||
doc.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||
self.assertEqual(doc.set_warehouse, None)
|
||||
# Mixed values
|
||||
doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 2"}, {"warehouse": "Warehouse 1"}]
|
||||
doc.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||
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)
|
||||
|
||||
data = frappe.db.sql("""select name, course, color,
|
||||
timestamp(schedule_date, from_time) as from_datetime,
|
||||
timestamp(schedule_date, to_time) as to_datetime,
|
||||
timestamp(schedule_date, from_time) as from_time,
|
||||
timestamp(schedule_date, to_time) as to_time,
|
||||
room, student_group, 0 as 'allDay'
|
||||
from `tabCourse Schedule`
|
||||
where ( schedule_date between %(start)s and %(end)s )
|
||||
|
@ -3,6 +3,8 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
@ -30,6 +32,14 @@ class CourseSchedule(Document):
|
||||
if self.from_time > self.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):
|
||||
"""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", "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"] = {
|
||||
field_map: {
|
||||
// from_datetime and to_datetime don't exist as docfields but are used in onload
|
||||
"start": "from_datetime",
|
||||
"end": "to_datetime",
|
||||
"start": "from_time",
|
||||
"end": "to_time",
|
||||
"id": "name",
|
||||
"title": "course",
|
||||
"allDay": "allDay"
|
||||
"allDay": "allDay",
|
||||
},
|
||||
gantt: false,
|
||||
order_by: "schedule_date",
|
||||
|
@ -6,6 +6,7 @@ import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import to_timedelta, today
|
||||
from frappe.utils.data import add_to_date
|
||||
|
||||
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,
|
||||
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):
|
||||
args = frappe._dict(args)
|
||||
|
||||
|
@ -19,7 +19,7 @@ class TestEmployeeOnboarding(unittest.TestCase):
|
||||
if frappe.db.exists('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 tabTask where project=%s", project)
|
||||
|
||||
@ -27,7 +27,7 @@ class TestEmployeeOnboarding(unittest.TestCase):
|
||||
onboarding = create_employee_onboarding()
|
||||
|
||||
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
|
||||
self.assertRaises(IncompleteTaskError, make_employee, onboarding.name)
|
||||
@ -64,8 +64,8 @@ class TestEmployeeOnboarding(unittest.TestCase):
|
||||
|
||||
|
||||
def get_job_applicant():
|
||||
if frappe.db.exists('Job Applicant', 'Test Researcher - test@researcher.com'):
|
||||
return frappe.get_doc('Job Applicant', 'Test Researcher - test@researcher.com')
|
||||
if frappe.db.exists('Job Applicant', 'test@researcher.com'):
|
||||
return frappe.get_doc('Job Applicant', 'test@researcher.com')
|
||||
applicant = frappe.new_doc('Job Applicant')
|
||||
applicant.applicant_name = 'Test Researcher'
|
||||
applicant.email_id = 'test@researcher.com'
|
||||
|
@ -192,10 +192,11 @@
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-29 23:06:10.904260",
|
||||
"modified": "2022-01-12 16:28:53.196881",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Job Applicant",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@ -210,10 +211,11 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"search_fields": "applicant_name",
|
||||
"search_fields": "applicant_name, email_id, job_title, phone_number",
|
||||
"sender_field": "email_id",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"subject_field": "notes",
|
||||
"title_field": "applicant_name"
|
||||
}
|
@ -7,6 +7,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.naming import append_number_if_name_exists
|
||||
from frappe.utils import validate_email_address
|
||||
|
||||
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
|
||||
|
||||
def autoname(self):
|
||||
keys = filter(None, (self.applicant_name, self.email_id, self.job_title))
|
||||
if not keys:
|
||||
frappe.throw(_("Name or Email is mandatory"), frappe.NameError)
|
||||
self.name = " - ".join(keys)
|
||||
self.name = self.email_id
|
||||
|
||||
# applicant can apply more than once for a different job title or reapply
|
||||
if frappe.db.exists("Job Applicant", self.name):
|
||||
self.name = append_number_if_name_exists("Job Applicant", self.name)
|
||||
|
||||
def validate(self):
|
||||
if self.email_id:
|
||||
|
@ -9,7 +9,26 @@ from erpnext.hr.doctype.designation.test_designation import create_designation
|
||||
|
||||
|
||||
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):
|
||||
args = frappe._dict(args)
|
||||
|
@ -1,294 +1,108 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "HR-LPR-.YYYY.-.#####",
|
||||
"beta": 0,
|
||||
"creation": "2018-04-13 15:20:52.864288",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"from_date",
|
||||
"to_date",
|
||||
"is_active",
|
||||
"column_break_3",
|
||||
"company",
|
||||
"optional_holiday_list"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "from_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_standard_filter": 0,
|
||||
"label": "From Date",
|
||||
"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": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "to_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_standard_filter": 0,
|
||||
"label": "To Date",
|
||||
"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": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "0",
|
||||
"fieldname": "is_active",
|
||||
"fieldtype": "Check",
|
||||
"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": "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
|
||||
"label": "Is Active"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "column_break_3",
|
||||
"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
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Company",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Company",
|
||||
"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
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "optional_holiday_list",
|
||||
"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",
|
||||
"length": 0,
|
||||
"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
|
||||
"options": "Holiday List"
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"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",
|
||||
"links": [],
|
||||
"modified": "2022-01-13 13:28:12.951025",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Leave Period",
|
||||
"name_case": "",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "HR Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "HR User",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"search_fields": "from_date, to_date, company",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0,
|
||||
"track_views": 0
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
@ -113,10 +113,11 @@
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-03-01 17:54:01.014509",
|
||||
"modified": "2022-01-13 13:37:11.218882",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Leave Policy Assignment",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@ -164,5 +165,7 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "employee_name",
|
||||
"track_changes": 1
|
||||
}
|
@ -48,7 +48,16 @@ frappe.listview_settings['Leave Policy Assignment'] = {
|
||||
if (cur_dialog.fields_dict.leave_period.value) {
|
||||
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"
|
||||
|
@ -530,16 +530,6 @@ class BOM(WebsiteGenerator):
|
||||
row.hour_rate = (hour_rate / flt(self.conversion_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:
|
||||
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
|
||||
|
@ -46,6 +46,7 @@ class TestRouting(ERPNextTestCase):
|
||||
wo_doc.delete()
|
||||
|
||||
def test_update_bom_operation_time(self):
|
||||
"""Update cost shouldn't update routing times."""
|
||||
operations = [
|
||||
{
|
||||
"operation": "Test Operation A",
|
||||
@ -85,8 +86,8 @@ class TestRouting(ERPNextTestCase):
|
||||
routing_doc.save()
|
||||
bom_doc.update_cost()
|
||||
bom_doc.reload()
|
||||
self.assertEqual(bom_doc.operations[0].time_in_mins, 90)
|
||||
self.assertEqual(bom_doc.operations[1].time_in_mins, 42.2)
|
||||
self.assertEqual(bom_doc.operations[0].time_in_mins, 30)
|
||||
self.assertEqual(bom_doc.operations[1].time_in_mins, 20)
|
||||
|
||||
|
||||
def setup_operations(rows):
|
||||
|
@ -2,7 +2,7 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
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.production_plan.test_production_plan import make_bom
|
||||
@ -12,6 +12,7 @@ from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||
OverProductionError,
|
||||
StockOverProductionError,
|
||||
close_work_order,
|
||||
make_job_card,
|
||||
make_stock_entry,
|
||||
stop_unstop,
|
||||
)
|
||||
@ -804,6 +805,34 @@ class TestWorkOrder(ERPNextTestCase):
|
||||
if row.is_scrap_item:
|
||||
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):
|
||||
items = ['Test FG Item for Closed WO', 'Test RM Item 1 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)
|
||||
|
||||
|
||||
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.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', {
|
||||
'from_time': now(),
|
||||
'employee': employee,
|
||||
'time_in_mins': 60,
|
||||
'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.shopify_deprecation_warning
|
||||
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.reset_clearance_date_for_intracompany_payment_entries
|
||||
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.requeue_failed_reposts
|
||||
erpnext.patches.v13_0.update_job_card_status
|
||||
erpnext.patches.v13_0.enable_uoms
|
||||
erpnext.patches.v12_0.update_production_plan_status
|
||||
erpnext.patches.v13_0.healthcare_deprecation_warning
|
||||
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.v13_0.agriculture_deprecation_warning
|
||||
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):
|
||||
frappe.delete_doc("Salary Slip", frappe.db.sql_list("""select name from `tabSalary Slip`
|
||||
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):
|
||||
"""
|
||||
|
@ -102,7 +102,7 @@ class Task(NestedSet):
|
||||
frappe.throw(_("Completed On cannot be greater than Today"))
|
||||
|
||||
def update_depends_on(self):
|
||||
depends_on_tasks = self.depends_on_tasks or ""
|
||||
depends_on_tasks = ""
|
||||
for d in self.depends_on:
|
||||
if d.task and d.task not in depends_on_tasks:
|
||||
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.base_amount - (soi.billed_amt * IFNULL(so.conversion_rate, 1))) as pending_amount,
|
||||
soi.warehouse as warehouse,
|
||||
so.company, soi.name
|
||||
so.company, soi.name,
|
||||
soi.description as description
|
||||
FROM
|
||||
`tabSales Order` so,
|
||||
(`tabSales Order Item` soi
|
||||
@ -184,6 +185,12 @@ def get_columns(filters):
|
||||
"options": "Item",
|
||||
"width": 100
|
||||
})
|
||||
columns.append({
|
||||
"label":_("Description"),
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"width": 100
|
||||
})
|
||||
|
||||
columns.extend([
|
||||
{
|
||||
|
@ -213,6 +213,9 @@ erpnext.company.setup_queries = function(frm) {
|
||||
["default_payroll_payable_account", {"root_type": "Liability"}],
|
||||
["round_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", {}],
|
||||
["discount_allowed_account", {"root_type": "Expense"}],
|
||||
["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 test_exchange_values.get(kwargs['params']['date']):
|
||||
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)
|
||||
class TestCurrencyExchange(unittest.TestCase):
|
||||
@ -102,6 +107,41 @@ class TestCurrencyExchange(unittest.TestCase):
|
||||
self.assertFalse(exchange_rate == 60)
|
||||
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):
|
||||
# strict currency settings
|
||||
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")
|
||||
|
||||
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():
|
||||
create_custom_field('Print Settings', {
|
||||
|
@ -353,7 +353,8 @@ def add_uom_data():
|
||||
"doctype": "UOM",
|
||||
"uom_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()
|
||||
|
||||
# bootstrap uom conversion factors
|
||||
|
@ -100,15 +100,21 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No
|
||||
|
||||
if not value:
|
||||
import requests
|
||||
api_url = "https://api.exchangerate.host/convert"
|
||||
response = requests.get(api_url, params={
|
||||
"date": transaction_date,
|
||||
"from": from_currency,
|
||||
"to": to_currency
|
||||
})
|
||||
settings = frappe.get_cached_doc('Currency Exchange Settings')
|
||||
req_params = {
|
||||
"transaction_date": transaction_date,
|
||||
"from_currency": from_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
|
||||
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))
|
||||
return flt(value)
|
||||
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))
|
||||
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():
|
||||
""" enable all roles and domain for testing """
|
||||
# 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)
|
||||
|
||||
def add_default_uom_in_conversion_factor_table(self):
|
||||
uom_conv_list = [d.uom for d in self.get("uoms")]
|
||||
if self.stock_uom not in uom_conv_list:
|
||||
ch = self.append('uoms', {})
|
||||
ch.uom = self.stock_uom
|
||||
ch.conversion_factor = 1
|
||||
if not self.is_new() and self.has_value_changed("stock_uom"):
|
||||
self.uoms = []
|
||||
frappe.msgprint(
|
||||
_("Successfully changed Stock UOM, please redefine conversion factors for new UOM."),
|
||||
alert=True,
|
||||
)
|
||||
|
||||
to_remove = []
|
||||
for d in self.get("uoms"):
|
||||
if d.conversion_factor == 1 and d.uom != self.stock_uom:
|
||||
to_remove.append(d)
|
||||
uoms_list = [d.uom for d in self.get("uoms")]
|
||||
|
||||
[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):
|
||||
if self.disabled:
|
||||
|
@ -584,6 +584,16 @@ class TestItem(ERPNextTestCase):
|
||||
except frappe.ValidationError as 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):
|
||||
self.assertRaises(frappe.ValidationError, validate_is_stock_item, "_Test Non Stock Item")
|
||||
|
||||
|
@ -8,6 +8,7 @@ from collections import defaultdict
|
||||
import frappe
|
||||
from frappe import _
|
||||
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
|
||||
|
||||
import erpnext
|
||||
@ -85,8 +86,11 @@ class StockEntry(StockController):
|
||||
self.validate_warehouse()
|
||||
self.validate_work_order()
|
||||
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_batch()
|
||||
self.validate_inspection()
|
||||
@ -109,8 +113,12 @@ class StockEntry(StockController):
|
||||
self.set_actual_qty()
|
||||
self.calculate_rate_and_amount()
|
||||
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):
|
||||
self.update_stock_ledger()
|
||||
@ -701,26 +709,25 @@ class StockEntry(StockController):
|
||||
validate_bom_no(item_code, d.bom_no)
|
||||
|
||||
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)]):
|
||||
return
|
||||
if any([d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)]):
|
||||
return
|
||||
|
||||
finished_item = self.get_finished_item()
|
||||
finished_item = self.get_finished_item()
|
||||
|
||||
if not finished_item and self.purpose == "Manufacture":
|
||||
# In case of independent Manufacture entry, don't auto set
|
||||
# user must decide and set
|
||||
return
|
||||
if not finished_item and self.purpose == "Manufacture":
|
||||
# In case of independent Manufacture entry, don't auto set
|
||||
# user must decide and set
|
||||
return
|
||||
|
||||
for d in self.items:
|
||||
if d.t_warehouse and not d.s_warehouse:
|
||||
if self.purpose=="Repack" or d.item_code == finished_item:
|
||||
d.is_finished_item = 1
|
||||
else:
|
||||
d.is_scrap_item = 1
|
||||
for d in self.items:
|
||||
if d.t_warehouse and not d.s_warehouse:
|
||||
if self.purpose=="Repack" or d.item_code == finished_item:
|
||||
d.is_finished_item = 1
|
||||
else:
|
||||
d.is_finished_item = 0
|
||||
d.is_scrap_item = 0
|
||||
d.is_scrap_item = 1
|
||||
else:
|
||||
d.is_finished_item = 0
|
||||
d.is_scrap_item = 0
|
||||
|
||||
def get_finished_item(self):
|
||||
finished_item = None
|
||||
@ -733,9 +740,9 @@ class StockEntry(StockController):
|
||||
|
||||
def validate_finished_goods(self):
|
||||
"""
|
||||
1. Check if FG exists
|
||||
2. Check if Multiple FG Items are present
|
||||
3. Check FG Item and Qty against WO if present
|
||||
1. Check if FG exists (mfg, repack)
|
||||
2. Check if Multiple FG Items are present (mfg)
|
||||
3. Check FG Item and Qty against WO if present (mfg)
|
||||
"""
|
||||
production_item, wo_qty, finished_items = None, 0, []
|
||||
|
||||
@ -748,8 +755,9 @@ class StockEntry(StockController):
|
||||
for d in self.get('items'):
|
||||
if d.is_finished_item:
|
||||
if not self.work_order:
|
||||
# Independent MFG Entry/ Repack Entry, no WO to match against
|
||||
finished_items.append(d.item_code)
|
||||
continue # Independent Manufacture Entry, no WO to match against
|
||||
continue
|
||||
|
||||
if d.item_code != production_item:
|
||||
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)
|
||||
|
||||
if len(set(finished_items)) > 1:
|
||||
if not finished_items:
|
||||
frappe.throw(
|
||||
msg=_("Multiple items cannot be marked as finished item"),
|
||||
title=_("Note"),
|
||||
exc=FinishedGoodError
|
||||
msg=_("There must be atleast 1 Finished Good in this Stock Entry").format(self.name),
|
||||
title=_("Missing Finished Good"), exc=FinishedGoodError
|
||||
)
|
||||
|
||||
if self.purpose == "Manufacture":
|
||||
if not finished_items:
|
||||
if len(set(finished_items)) > 1:
|
||||
frappe.throw(
|
||||
msg=_("There must be atleast 1 Finished Good in this Stock Entry").format(self.name),
|
||||
title=_("Missing Finished Good"),
|
||||
exc=FinishedGoodError
|
||||
msg=_("Multiple items cannot be marked as finished item"),
|
||||
title=_("Note"), exc=FinishedGoodError
|
||||
)
|
||||
|
||||
allowance_percentage = flt(
|
||||
@ -1275,22 +1281,29 @@ class StockEntry(StockController):
|
||||
if not self.pro_doc:
|
||||
self.set_work_order_details()
|
||||
|
||||
scrap_items = frappe.db.sql('''
|
||||
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:
|
||||
if not self.pro_doc.operations:
|
||||
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()
|
||||
for row in scrap_items:
|
||||
row.stock_qty -= flt(used_scrap_items.get(row.item_code))
|
||||
@ -1304,6 +1317,9 @@ class StockEntry(StockController):
|
||||
|
||||
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):
|
||||
used_scrap_items = defaultdict(float)
|
||||
data = frappe.get_all(
|
||||
|
@ -226,9 +226,47 @@ class TestStockEntry(ERPNextTestCase):
|
||||
|
||||
mtn.cancel()
|
||||
|
||||
def test_repack_no_change_in_valuation(self):
|
||||
company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company')
|
||||
def test_repack_multiple_fg(self):
|
||||
"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 Home Desktop 100", target="_Test Warehouse - _TC",
|
||||
qty=50, basic_rate=100)
|
||||
|
@ -853,7 +853,7 @@ def get_user_time(user, to_string=False):
|
||||
@frappe.whitelist()
|
||||
def get_sla_doctypes():
|
||||
doctypes = []
|
||||
data = frappe.get_list('Service Level Agreement',
|
||||
data = frappe.get_all('Service Level Agreement',
|
||||
{'enabled': 1},
|
||||
['document_type'],
|
||||
distinct=1
|
||||
|
@ -125,17 +125,23 @@ def execute_script_report(
|
||||
if default_filters is None:
|
||||
default_filters = {}
|
||||
|
||||
test_filters = []
|
||||
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_data = report_execute_fn(report_filters)
|
||||
test_filters.append(report_filters)
|
||||
|
||||
if optional_filters:
|
||||
for key, value in optional_filters.items():
|
||||
filter_with_optional_param = report_filters.copy().update({key: value})
|
||||
report_execute_fn(filter_with_optional_param)
|
||||
test_filters.append(report_filters.copy().update({key: value}))
|
||||
|
||||
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."):
|
||||
|
@ -181,8 +181,6 @@ class TransactionBase(StatusUpdater):
|
||||
|
||||
if len(child_table_values) > 1:
|
||||
self.set(default_field, None)
|
||||
else:
|
||||
self.set(default_field, list(child_table_values)[0])
|
||||
|
||||
def delete_events(ref_type, ref_name):
|
||||
events = frappe.db.sql_list(""" SELECT
|
||||
|
@ -1,6 +1,6 @@
|
||||
# frappe # https://github.com/frappe/frappe is installed during bench-init
|
||||
gocardless-pro~=1.22.0
|
||||
googlemaps # used in ERPNext, but dependency is defined in Frappe
|
||||
googlemaps
|
||||
pandas~=1.1.5
|
||||
plaid-python~=7.2.1
|
||||
pycountry~=20.7.3
|
||||
|
Loading…
x
Reference in New Issue
Block a user