Merge branch 'develop' of https://github.com/frappe/erpnext into nil_exempt_non_gst_gstr_1

This commit is contained in:
Deepesh Garg 2022-01-13 22:40:20 +05:30
commit 1846dbfb4d
66 changed files with 1076 additions and 374 deletions

View File

@ -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
View 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']

View File

@ -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
View 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 }}"

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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();
}

View File

@ -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
}

View File

@ -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))

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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':

View File

@ -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

View File

@ -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

View File

@ -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({

View File

@ -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",

View File

@ -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):
"""

View File

@ -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",

View File

@ -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",

View 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,
)

View File

@ -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

View File

@ -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)

View File

@ -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")

View File

@ -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 )

View File

@ -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)

View File

@ -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",

View File

@ -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)

View File

@ -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'

View File

@ -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"
}

View File

@ -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:

View File

@ -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)

View File

@ -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
}

View File

@ -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
}

View File

@ -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"

View File

@ -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

View File

@ -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):

View File

@ -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
})

View File

@ -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

View 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()

View 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)

View 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()

View File

@ -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):
"""

View File

@ -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 + ","

View File

@ -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([
{

View File

@ -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"}],

View File

@ -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)

View File

@ -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', {

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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")

View File

@ -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(

View File

@ -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)

View File

@ -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

View File

@ -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."):

View File

@ -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

View File

@ -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