mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-04 09:00:21 +00:00
Compare commits
10 Commits
version-16
...
version-16
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48983c5ef0 | ||
|
|
d23b407ec7 | ||
|
|
770726d8f5 | ||
|
|
e5b1ff667d | ||
|
|
d8236548be | ||
|
|
f75601e9b1 | ||
|
|
0684599bdb | ||
|
|
a1c8dc878d | ||
|
|
dfb64d7635 | ||
|
|
b0ddca0455 |
30
.github/helper/install.sh
vendored
30
.github/helper/install.sh
vendored
@@ -4,6 +4,36 @@ set -e
|
||||
|
||||
cd ~ || exit
|
||||
|
||||
# Authenticate git against github.com with the job token: anonymous git-over-HTTPS from the
|
||||
# runners gets throttled to a 401, which kills whichever clone is in flight — the frappe fetch
|
||||
# below, or payments under `bench get-app`. See the PR description.
|
||||
#
|
||||
# A credential helper rather than a url.insteadOf rewrite, because `git clone` PERSISTS a
|
||||
# rewritten URL into the new repo's .git/config: an insteadOf would leave the token sitting in
|
||||
# apps/payments/.git/config on the runner. A helper is consulted only when github.com actually
|
||||
# challenges, and leaves the stored remote URL untouched. Passing it through GIT_CONFIG_* keeps
|
||||
# the token out of ~/.gitconfig too, and child processes inherit it (bench shells out to git).
|
||||
ci_github_token=${CI_GITHUB_TOKEN:-${GITHUB_TOKEN:-}}
|
||||
if [ -n "$ci_github_token" ]; then
|
||||
export CI_GITHUB_TOKEN="$ci_github_token"
|
||||
export GIT_CONFIG_COUNT=3
|
||||
# Reset first: git runs EVERY configured helper and calls `store` on them after a successful
|
||||
# auth, so a `credential.helper=store` inherited from the image's gitconfig would write the
|
||||
# token to ~/.git-credentials. An empty value clears the list before ours is added.
|
||||
export GIT_CONFIG_KEY_0="credential.helper"
|
||||
export GIT_CONFIG_VALUE_0=""
|
||||
export GIT_CONFIG_KEY_1="credential.https://github.com.username"
|
||||
export GIT_CONFIG_VALUE_1="x-access-token"
|
||||
export GIT_CONFIG_KEY_2="credential.https://github.com.helper"
|
||||
# Single-quoted: $CI_GITHUB_TOKEN is expanded by the shell git runs the helper in, so the
|
||||
# token is read from the environment at call time and never stored anywhere. Answering only
|
||||
# `get` makes the helper inert for git's `store`/`erase` calls.
|
||||
export GIT_CONFIG_VALUE_2='!f() { test "$1" = get && echo "password=$CI_GITHUB_TOKEN"; }; f'
|
||||
fi
|
||||
|
||||
# Whatever happens, never sit on a credential prompt: fail fast and legibly instead.
|
||||
export GIT_TERMINAL_PROMPT=0
|
||||
|
||||
githubbranch=${GITHUB_BASE_REF:-${GITHUB_REF##*/}}
|
||||
frappeuser=${FRAPPE_USER:-"frappe"}
|
||||
frappecommitish=${FRAPPE_BRANCH:-$githubbranch}
|
||||
|
||||
2
.github/workflows/patch.yml
vendored
2
.github/workflows/patch.yml
vendored
@@ -105,6 +105,8 @@ jobs:
|
||||
env:
|
||||
DB: mariadb
|
||||
TYPE: server
|
||||
# Anonymous git to github.com gets throttled to a 401; authenticate the clones.
|
||||
CI_GITHUB_TOKEN: ${{ github.token }}
|
||||
|
||||
- name: Run Patch Tests
|
||||
run: |
|
||||
|
||||
2
.github/workflows/run-individual-tests.yml
vendored
2
.github/workflows/run-individual-tests.yml
vendored
@@ -129,6 +129,8 @@ jobs:
|
||||
TYPE: server
|
||||
FRAPPE_USER: ${{ github.event.inputs.user }}
|
||||
FRAPPE_BRANCH: ${{ github.event.inputs.branch }}
|
||||
# Anonymous git to github.com gets throttled to a 401; authenticate the clones.
|
||||
CI_GITHUB_TOKEN: ${{ github.token }}
|
||||
|
||||
- name: Run Tests
|
||||
run: |
|
||||
|
||||
2
.github/workflows/server-tests-mariadb.yml
vendored
2
.github/workflows/server-tests-mariadb.yml
vendored
@@ -102,6 +102,8 @@ jobs:
|
||||
TYPE: server
|
||||
FRAPPE_USER: ${{ github.event.inputs.user }}
|
||||
FRAPPE_BRANCH: ${{ github.event.client_payload.sha || github.event.inputs.branch }}
|
||||
# Anonymous git to github.com gets throttled to a 401; authenticate the clones.
|
||||
CI_GITHUB_TOKEN: ${{ github.token }}
|
||||
DB_HOST: 127.0.0.1
|
||||
DB_USER_HOST: '%'
|
||||
WKHTMLTOX_DEB: /tmp/wkhtmltox.deb
|
||||
|
||||
@@ -193,8 +193,10 @@ class TemplateStructureValidator(Validator):
|
||||
if not row.calculation_formula:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=_("{0} is required for {1}").format(
|
||||
get_formula_field_label(row.data_source), row.data_source
|
||||
message=_("{0} is required when {1} is {2}").format(
|
||||
get_formula_field_label(row.data_source),
|
||||
row.meta.get_translated_label("data_source"),
|
||||
_(row.data_source),
|
||||
),
|
||||
row_idx=row.idx,
|
||||
)
|
||||
@@ -222,7 +224,14 @@ class DependencyValidator(Validator):
|
||||
|
||||
for row in self.template.rows:
|
||||
if row.reference_code and row.data_source == "Calculated Amount" and row.calculation_formula:
|
||||
deps = extract_reference_codes_from_formula(row.calculation_formula, list(available_codes))
|
||||
# skip self-reference, `CalculationFormulaValidator` already reports it
|
||||
deps = [
|
||||
code
|
||||
for code in extract_reference_codes_from_formula(
|
||||
row.calculation_formula, list(available_codes)
|
||||
)
|
||||
if code != row.reference_code
|
||||
]
|
||||
if deps:
|
||||
graph[row.reference_code] = deps
|
||||
|
||||
@@ -284,7 +293,9 @@ class DependencyValidator(Validator):
|
||||
row_idx = self._get_row_idx(ref_code)
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=_("Line References undefined in Formula: {0}").format(", ".join(undefined)),
|
||||
message=_("Line references undefined in {0}: {1}").format(
|
||||
get_formula_field_label("Calculated Amount"), ", ".join(undefined)
|
||||
),
|
||||
row_idx=row_idx,
|
||||
)
|
||||
)
|
||||
@@ -311,17 +322,6 @@ class CalculationFormulaValidator(Validator):
|
||||
if row.data_source != "Calculated Amount":
|
||||
return result
|
||||
|
||||
if not row.calculation_formula:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=_("{0} is required for Calculated Amount").format(
|
||||
get_formula_field_label(row.data_source)
|
||||
),
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
formula = self._preprocess_formula(row.calculation_formula)
|
||||
row.calculation_formula = formula
|
||||
|
||||
@@ -346,16 +346,6 @@ class CalculationFormulaValidator(Validator):
|
||||
)
|
||||
)
|
||||
|
||||
# Check undefined references
|
||||
undefined = set(refs) - set(available_codes)
|
||||
if undefined:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=_("Formula references undefined codes: {0}").format(", ".join(undefined)),
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
|
||||
# Try to evaluate with dummy values
|
||||
eval_error = self._test_formula_evaluation(formula, available_codes)
|
||||
if eval_error:
|
||||
@@ -418,17 +408,6 @@ class AccountFilterValidator(Validator):
|
||||
if row.data_source != "Account Data":
|
||||
return result
|
||||
|
||||
if not row.calculation_formula:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=_("{0} is required for Account Data").format(
|
||||
get_formula_field_label(row.data_source)
|
||||
),
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
try:
|
||||
filter_config = json.loads(row.calculation_formula)
|
||||
error = self._validate_filter_structure(
|
||||
@@ -440,7 +419,9 @@ class AccountFilterValidator(Validator):
|
||||
if error:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=_("{0}: {1}").format(get_formula_field_label(row.data_source), error),
|
||||
message=_("[{0}] {1}", context="Financial Report Template").format(
|
||||
get_formula_field_label(row.data_source), error
|
||||
),
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
@@ -448,8 +429,9 @@ class AccountFilterValidator(Validator):
|
||||
except json.JSONDecodeError as e:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=_("{0}: Invalid JSON format: {1}").format(
|
||||
get_formula_field_label(row.data_source), str(e)
|
||||
message=_("[{0}] {1}", context="Financial Report Template").format(
|
||||
get_formula_field_label(row.data_source),
|
||||
_("Invalid JSON format: {0}").format(str(e)),
|
||||
),
|
||||
row_idx=row.idx,
|
||||
)
|
||||
@@ -555,8 +537,9 @@ class FormulaValidator(Validator):
|
||||
frappe.clear_last_message()
|
||||
|
||||
if isinstance(e, frappe.PermissionError):
|
||||
message = _("{0}: Method '{1}' must be whitelisted and permit GET requests").format(
|
||||
get_formula_field_label(row.data_source), api_path
|
||||
message = _("[{0}] {1}", context="Financial Report Template").format(
|
||||
get_formula_field_label(row.data_source),
|
||||
_("Method '{0}' must be whitelisted and permit GET requests").format(api_path),
|
||||
)
|
||||
else:
|
||||
message = _("Could not validate {0}: {1}").format(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
import datetime
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_to_date, now_datetime, nowdate
|
||||
@@ -9,12 +9,20 @@ from frappe.utils import add_to_date, now_datetime, nowdate
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.projects.doctype.task.test_task import create_task
|
||||
from erpnext.projects.doctype.timesheet.timesheet import OverlapError, make_sales_invoice
|
||||
from erpnext.projects.doctype.timesheet.timesheet import (
|
||||
OverlapError,
|
||||
get_projectwise_timesheet_data,
|
||||
make_sales_invoice,
|
||||
)
|
||||
from erpnext.setup.doctype.employee.test_employee import make_employee
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestTimesheet(ERPNextTestSuite):
|
||||
def test_get_projectwise_timesheet_data_without_allowed_projects(self):
|
||||
with patch("frappe.get_list", side_effect=[["TS-0001"], []]):
|
||||
self.assertEqual(get_projectwise_timesheet_data(), [])
|
||||
|
||||
def test_timesheet_post_update(self):
|
||||
frappe.get_doc(
|
||||
{
|
||||
|
||||
@@ -335,10 +335,14 @@ def get_projectwise_timesheet_data(project=None, parent=None, from_time=None, to
|
||||
& (tsd.is_billable == 1)
|
||||
& tsd.sales_invoice.isnull()
|
||||
& (tsd.parent.isin(allowed_timesheets))
|
||||
& ((tsd.project.isin(allowed_projects)) | (tsd.project.isnull()))
|
||||
)
|
||||
)
|
||||
|
||||
if allowed_projects:
|
||||
query = query.where((tsd.project.isin(allowed_projects)) | (tsd.project.isnull()))
|
||||
else:
|
||||
query = query.where(tsd.project.isnull())
|
||||
|
||||
if project:
|
||||
query = query.where(tsd.project == project)
|
||||
if parent:
|
||||
|
||||
@@ -1082,6 +1082,8 @@ def get_billing_shipping_address(name, billing_address=None, shipping_address=No
|
||||
@frappe.whitelist()
|
||||
def create_transaction_deletion_request(company):
|
||||
frappe.only_for("System Manager")
|
||||
# User Permission check
|
||||
frappe.has_permission("Company", ptype="delete", doc=company, throw=True)
|
||||
|
||||
from erpnext.setup.doctype.transaction_deletion_record.transaction_deletion_record import (
|
||||
is_deletion_doc_running,
|
||||
@@ -1090,6 +1092,7 @@ def create_transaction_deletion_request(company):
|
||||
is_deletion_doc_running(company)
|
||||
|
||||
tdr = frappe.get_doc({"doctype": "Transaction Deletion Record", "company": company})
|
||||
tdr.flags.ignore_permissions = 1
|
||||
tdr.insert()
|
||||
|
||||
tdr.generate_to_delete_list()
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_bulk_edit": 1,
|
||||
"autoname": "TDL.####",
|
||||
"creation": "2021-04-06 20:17:18.404716",
|
||||
"doctype": "DocType",
|
||||
@@ -166,19 +167,18 @@
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"in_create": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-11-18 15:02:46.427695",
|
||||
"modified": "2026-09-02 20:32:19.679290",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Transaction Deletion Record",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"naming_rule": "Expression",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
@@ -186,7 +186,6 @@
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"submit": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
|
||||
@@ -5,12 +5,12 @@ frappe.listview_settings["Batch"] = {
|
||||
return [__("Disabled"), "gray", "disabled,=,1"];
|
||||
} else if (
|
||||
doc.expiry_date &&
|
||||
frappe.datetime.get_diff(doc.expiry_date, frappe.datetime.nowdate()) <= 0
|
||||
frappe.datetime.get_diff(doc.expiry_date, frappe.datetime.nowdate()) < 0
|
||||
) {
|
||||
return [
|
||||
__("Expired"),
|
||||
"red",
|
||||
"expiry_date,not in,|expiry_date,<=,Today|batch_qty,>,0|disabled,=,0",
|
||||
"expiry_date,not in,|expiry_date,<,Today|batch_qty,>,0|disabled,=,0",
|
||||
];
|
||||
} else if (!doc.batch_qty) {
|
||||
return [__("Empty"), "gray", "batch_qty,=,0|disabled,=,0"];
|
||||
|
||||
@@ -101,8 +101,27 @@ frappe.ui.form.on("Material Request", {
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
|
||||
if (!frm.doc.buying_price_list) {
|
||||
const buying_price_list = frappe.defaults.get_default("buying_price_list");
|
||||
if (frappe.has_permission("Price List", "read", buying_price_list)) {
|
||||
frm.set_value("buying_price_list", buying_price_list);
|
||||
if (buying_price_list) {
|
||||
const docname = frm.doc.name;
|
||||
frappe.call({
|
||||
type: "GET",
|
||||
method: "frappe.client.has_permission",
|
||||
no_spinner: true,
|
||||
args: {
|
||||
doctype: "Price List",
|
||||
docname: buying_price_list,
|
||||
perm_type: "read",
|
||||
},
|
||||
callback: ({ message }) => {
|
||||
if (
|
||||
message?.has_permission &&
|
||||
frm.doc.name === docname &&
|
||||
!frm.doc.buying_price_list
|
||||
) {
|
||||
frm.set_value("buying_price_list", buying_price_list);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -189,7 +189,7 @@ class StockClosingEntry(Document):
|
||||
new_doc.posting_datetime = get_combine_datetime(self.to_date, new_doc.posting_time)
|
||||
new_doc.stock_closing_entry = self.name
|
||||
new_doc.company = self.company
|
||||
new_doc.save()
|
||||
new_doc.save(ignore_permissions=True)
|
||||
|
||||
def get_prepared_data(self):
|
||||
if attachments := get_attachments(self.doctype, self.name):
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from unittest.mock import patch
|
||||
|
||||
import frappe
|
||||
from frappe.core.doctype.user_permission.test_user_permission import create_user
|
||||
from frappe.utils import today
|
||||
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
# On ERPNextTestSuite, the doctype test records and all
|
||||
# link-field test record depdendencies are recursively loaded
|
||||
# Use these module variables to add/remove to/from that list
|
||||
|
||||
COMPANY = "_Test Company"
|
||||
WAREHOUSE = "_Test Warehouse - _TC"
|
||||
|
||||
|
||||
class TestStockClosingEntry(ERPNextTestSuite):
|
||||
"""
|
||||
@@ -16,4 +24,40 @@ class TestStockClosingEntry(ERPNextTestSuite):
|
||||
Use this class for testing interactions between multiple components.
|
||||
"""
|
||||
|
||||
pass
|
||||
def make_stock_closing_entry(self, from_date, to_date):
|
||||
entry = frappe.get_doc(
|
||||
doctype="Stock Closing Entry",
|
||||
company=COMPANY,
|
||||
from_date=from_date,
|
||||
to_date=to_date,
|
||||
).submit()
|
||||
self.last_closing_entry = entry.name
|
||||
return entry
|
||||
|
||||
def test_non_administrator_can_generate_closing_balance(self):
|
||||
item = make_item(properties={"is_stock_item": 1}).name
|
||||
with patch("erpnext.stock.doctype.stock_closing_entry.stock_closing_entry.enqueue"):
|
||||
entry = self.make_stock_closing_entry(today(), today())
|
||||
|
||||
user = create_user("test_stock_closing_balance@example.com", "Stock User")
|
||||
self.assertFalse(frappe.has_permission("Stock Closing Balance", "create", user=user.name))
|
||||
|
||||
balance = frappe._dict(
|
||||
item_code=item,
|
||||
warehouse=WAREHOUSE,
|
||||
actual_qty=1,
|
||||
stock_value_difference=100,
|
||||
fifo_queue=None,
|
||||
)
|
||||
with (
|
||||
patch(
|
||||
"erpnext.stock.doctype.stock_closing_entry.stock_closing_entry.StockClosing"
|
||||
) as stock_closing,
|
||||
self.set_user(user.name),
|
||||
):
|
||||
stock_closing.return_value.get_stock_closing_entries.return_value = {(item, WAREHOUSE): balance}
|
||||
entry.create_stock_closing_balance_entries()
|
||||
|
||||
self.assertTrue(
|
||||
frappe.db.exists("Stock Closing Balance", {"stock_closing_entry": entry.name, "item_code": item})
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user