mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-05 01:20:22 +00:00
Compare commits
2 Commits
version-16
...
pr-58450
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c49c1a18df | ||
|
|
7b8d432032 |
@@ -1,54 +0,0 @@
|
||||
name: Upload main.pot to Crowdin
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- version-16-hotfix
|
||||
paths:
|
||||
- "erpnext/locale/main.pot"
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: crowdin-upload-${{ github.ref_name }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
upload-sources:
|
||||
name: Upload sources from ${{ github.ref_name }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout ${{ github.ref_name }}
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Restore Crowdin cache
|
||||
uses: actions/cache/restore@v6
|
||||
with:
|
||||
path: .crowdin
|
||||
key: crowdin-${{ github.ref_name }}
|
||||
restore-keys: crowdin-${{ github.ref_name }}-
|
||||
|
||||
- name: Upload main.pot to Crowdin
|
||||
uses: crowdin/github-action@8f01d54f70f1713ee3f09d82c2bbb2daeac28689 # v2.17.1
|
||||
with:
|
||||
config: crowdin.yml
|
||||
upload_sources: true
|
||||
upload_translations: false
|
||||
download_translations: false
|
||||
create_pull_request: false
|
||||
crowdin_branch_name: "[frappe.erpnext] ${{ github.ref_name }}"
|
||||
upload_sources_args: "--cache"
|
||||
env:
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
|
||||
- name: Save Crowdin cache
|
||||
uses: actions/cache/save@v6
|
||||
if: always()
|
||||
with:
|
||||
path: .crowdin
|
||||
key: crowdin-${{ github.ref_name }}-${{ github.run_id }}
|
||||
@@ -13,7 +13,6 @@ on:
|
||||
- 'crowdin.yml'
|
||||
- '.coderabbit.yml'
|
||||
- '.mergify.yml'
|
||||
- '**.po'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
1
.github/workflows/server-tests-mariadb.yml
vendored
1
.github/workflows/server-tests-mariadb.yml
vendored
@@ -13,7 +13,6 @@ on:
|
||||
- 'crowdin.yml'
|
||||
- '.coderabbit.yml'
|
||||
- '.mergify.yml'
|
||||
- '**.po'
|
||||
schedule:
|
||||
# Run everday at midnight UTC / 5:30 IST
|
||||
- cron: "0 0 * * *"
|
||||
|
||||
13
crowdin.yml
13
crowdin.yml
@@ -1,5 +1,14 @@
|
||||
preserve_hierarchy: true
|
||||
|
||||
files:
|
||||
- source: /erpnext/locale/main.pot
|
||||
translation: /erpnext/locale/%two_letters_code%.po
|
||||
pull_request_title: "fix: sync translations from crowdin"
|
||||
pull_request_labels:
|
||||
- translation
|
||||
- skip-release-notes
|
||||
pull_request_reviewers:
|
||||
- barredterra # change to your GitHub username if you copied this file
|
||||
commit_message: "fix: %language% translations"
|
||||
append_commit_message: false
|
||||
languages_mapping:
|
||||
two_letters_code:
|
||||
pt-BR: pt_BR
|
||||
|
||||
@@ -6,7 +6,7 @@ import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "16.34.1"
|
||||
__version__ = "16.26.2"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -234,10 +234,8 @@ frappe.ui.form.on("Dunning", {
|
||||
dn: frm.doc.name,
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.exc) {
|
||||
var doc = frappe.model.sync(r.message);
|
||||
frappe.set_route("Form", doc[0].doctype, doc[0].name);
|
||||
}
|
||||
var doc = frappe.model.sync(r.message);
|
||||
frappe.set_route("Form", doc[0].doctype, doc[0].name);
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -31,7 +31,6 @@ from erpnext.accounts.doctype.financial_report_template.financial_report_validat
|
||||
AccountFilterValidator,
|
||||
CalculationFormulaValidator,
|
||||
DependencyValidator,
|
||||
get_valid_api_method,
|
||||
)
|
||||
from erpnext.accounts.report.financial_statements import (
|
||||
get_columns,
|
||||
@@ -1165,12 +1164,10 @@ class RowProcessor:
|
||||
|
||||
def _process_api_row(self, row) -> RowData:
|
||||
api_path = row.calculation_formula
|
||||
|
||||
method = get_valid_api_method(api_path)
|
||||
# TODO
|
||||
|
||||
try:
|
||||
# nosemgrep: frappe-semgrep-rules.rules.security.frappe-codeinjection-eval
|
||||
values = frappe.call(method, filters=self.context.filters, periods=self.period_list, row=row)
|
||||
values = frappe.call(api_path, filters=self.context.filters, periods=self.period_list, row=row)
|
||||
|
||||
if row.reverse_sign:
|
||||
values = [-1 * v for v in values]
|
||||
|
||||
@@ -163,7 +163,7 @@ function show_accounts_tree(template_rows, has_selection) {
|
||||
fieldname: "company",
|
||||
fieldtype: "Link",
|
||||
options: "Company",
|
||||
label: __("Company"),
|
||||
label: "Company",
|
||||
reqd: 1,
|
||||
default: frappe.defaults.get_user_default("Company"),
|
||||
onchange: () => {
|
||||
@@ -176,7 +176,7 @@ function show_accounts_tree(template_rows, has_selection) {
|
||||
fieldname: "view_type",
|
||||
fieldtype: "Select",
|
||||
options: ["Missing Accounts", "Filtered Accounts"],
|
||||
label: __("View"),
|
||||
label: "View",
|
||||
default: has_selection ? "Filtered Accounts" : "Missing Accounts",
|
||||
reqd: 1,
|
||||
onchange: () => {
|
||||
@@ -192,10 +192,10 @@ function show_accounts_tree(template_rows, has_selection) {
|
||||
{
|
||||
fieldname: "tip",
|
||||
fieldtype: "HTML",
|
||||
label: __("Tip"),
|
||||
label: "Tip",
|
||||
options: `
|
||||
<div class="alert alert-success" role="alert">
|
||||
${__("Tip: Select report lines to view their accounts")}
|
||||
Tip: Select report lines to view their accounts
|
||||
</div>
|
||||
`,
|
||||
depends_on: has_selection ? "eval: false" : "eval: true",
|
||||
@@ -203,7 +203,7 @@ function show_accounts_tree(template_rows, has_selection) {
|
||||
{
|
||||
fieldname: "tree_area",
|
||||
fieldtype: "HTML",
|
||||
label: __("Chart of Accounts"),
|
||||
label: "Chart of Accounts",
|
||||
read_only: 1,
|
||||
depends_on: "eval: doc.company",
|
||||
},
|
||||
@@ -288,14 +288,14 @@ function update_formula_label(frm, data_source) {
|
||||
if (!field) return;
|
||||
|
||||
const labels = {
|
||||
"Account Data": __("Account Filter"),
|
||||
"Custom API": __("API Method Path"),
|
||||
"Account Data": "Account Filter",
|
||||
"Custom API": "API Method Path",
|
||||
};
|
||||
|
||||
grid.update_docfield_property(
|
||||
"calculation_formula",
|
||||
"label",
|
||||
labels[data_source] || __("Calculation Formula")
|
||||
labels[data_source] || "Calculation Formula"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -370,7 +370,7 @@ function update_formula_description(frm, data_source) {
|
||||
description_html = `
|
||||
<div ${container_style}>
|
||||
<h5 ${title_style}>Custom API Setup</h5>
|
||||
<p ${text_style}>Path to your custom whitelisted method that returns financial data. It must permit GET requests.</p>
|
||||
<p ${text_style}>Path to your custom method that returns financial data.</p>
|
||||
|
||||
<h6 ${subtitle_style}>Format:</h6>
|
||||
<ul ${list_style}>
|
||||
@@ -380,8 +380,7 @@ function update_formula_description(frm, data_source) {
|
||||
|
||||
<h6 ${subtitle_style}>Method Signature:</h6>
|
||||
<div ${code_style}>
|
||||
<!-- is used for line breaks since frappe.render replaces newlines with spaces -->
|
||||
<pre ${pre_style} class="language-python">@frappe.whitelist(methods=["GET"]) def get_custom_data(filters, periods, row): # filters: dict — report filters (company, period, etc.) # periods: list[dict] — period definitions # row: dict — the current report row return [1000.0, 1200.0, 1150.0] # one value per period</pre>
|
||||
<pre ${pre_style}>def get_custom_data(filters, periods, row): <br> # filters: dict — report filters (company, period, etc.) <br> # periods: list[dict] — period definitions <br> # row: dict — the current report row <br><br> return [1000.0, 1200.0, 1150.0] # one value per period</pre>
|
||||
</div>
|
||||
|
||||
<h6 ${subtitle_style}>Return Format:</h6>
|
||||
|
||||
@@ -10,41 +10,18 @@ from enum import Enum
|
||||
from typing import Any, ClassVar
|
||||
|
||||
import frappe
|
||||
from frappe import _, is_whitelisted
|
||||
from frappe import _
|
||||
from frappe.database.operator_map import OPERATOR_MAP
|
||||
from frappe.database.query import SQLFunctionParser
|
||||
|
||||
|
||||
def get_valid_api_method(api_path: str):
|
||||
"""Resolve `api_path`, ensuring it is whitelisted and permits GET (i.e. read-only)."""
|
||||
method = frappe.get_attr(api_path)
|
||||
is_whitelisted(method)
|
||||
|
||||
if "GET" not in frappe.allowed_http_methods_for_whitelisted_func.get(method, ()):
|
||||
frappe.throw(
|
||||
_("Method {0} must permit GET requests").format(frappe.bold(api_path)),
|
||||
frappe.PermissionError,
|
||||
title=_("Method Not Allowed"),
|
||||
)
|
||||
|
||||
return method
|
||||
|
||||
|
||||
def get_formula_field_label(data_source: str) -> str:
|
||||
# Must mirror the `labels` map in financial_report_template.js (update_formula_label),
|
||||
labels = {
|
||||
"Account Data": _("Account Filter"),
|
||||
"Custom API": _("API Method Path"),
|
||||
}
|
||||
return labels.get(data_source, _("Calculation Formula"))
|
||||
|
||||
|
||||
@dataclass
|
||||
class ValidationIssue:
|
||||
"""Represents a single validation issue"""
|
||||
|
||||
message: str
|
||||
row_idx: int | None = None
|
||||
field: str | None = None
|
||||
details: dict[str, Any] = None
|
||||
|
||||
def __post_init__(self):
|
||||
@@ -52,9 +29,10 @@ class ValidationIssue:
|
||||
self.details = {}
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.row_idx:
|
||||
return _("Row {0}: {1}", context="Financial Report Template").format(self.row_idx, self.message)
|
||||
return self.message
|
||||
prefix = f"Row {self.row_idx}: " if self.row_idx else ""
|
||||
field_info = f"[{self.field}] " if self.field else ""
|
||||
message = f"{prefix}{field_info}{self.message}"
|
||||
return _(message)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -156,9 +134,7 @@ class TemplateStructureValidator(Validator):
|
||||
if not re.match(r"^[A-Za-z][A-Za-z0-9_-]*$", ref_code):
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=_(
|
||||
"Invalid line reference format: '{0}'. Must start with letter and contain only letters, numbers, underscores, and hyphens"
|
||||
).format(ref_code),
|
||||
message=f"Invalid line reference format: '{ref_code}'. Must start with letter and contain only letters, numbers, underscores, and hyphens",
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
@@ -167,7 +143,7 @@ class TemplateStructureValidator(Validator):
|
||||
if ref_code in used_codes:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=_("Duplicate line reference: '{0}'").format(ref_code),
|
||||
message=f"Duplicate line reference: '{ref_code}'",
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
@@ -183,7 +159,7 @@ class TemplateStructureValidator(Validator):
|
||||
if row.data_source == "Account Data" and not row.balance_type:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=_("Balance Type is required for Account Data"),
|
||||
message="Balance Type is required for Account Data",
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
@@ -193,9 +169,7 @@ 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=f"Formula is required for {row.data_source}",
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
@@ -252,7 +226,7 @@ class DependencyValidator(Validator):
|
||||
cycle = [*path[cycle_start:], node]
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=_("Circular dependency detected: {0}").format(" → ".join(cycle)),
|
||||
message=f"Circular dependency detected: {' → '.join(cycle)}",
|
||||
)
|
||||
)
|
||||
return
|
||||
@@ -284,7 +258,7 @@ 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=f"Line References undefined in Formula: {', '.join(undefined)}",
|
||||
row_idx=row_idx,
|
||||
)
|
||||
)
|
||||
@@ -314,10 +288,9 @@ class CalculationFormulaValidator(Validator):
|
||||
if not row.calculation_formula:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=_("{0} is required for Calculated Amount").format(
|
||||
get_formula_field_label(row.data_source)
|
||||
),
|
||||
message="Formula is required for Calculated Amount",
|
||||
row_idx=row.idx,
|
||||
field="Formula",
|
||||
)
|
||||
)
|
||||
return result
|
||||
@@ -329,7 +302,7 @@ class CalculationFormulaValidator(Validator):
|
||||
if not self._are_parentheses_balanced(formula):
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=_("Formula has unbalanced parentheses"),
|
||||
message="Formula has unbalanced parentheses",
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
@@ -341,7 +314,7 @@ class CalculationFormulaValidator(Validator):
|
||||
if row.reference_code and row.reference_code in refs:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=_("Formula references itself ('{0}')").format(row.reference_code),
|
||||
message=f"Formula references itself ('{row.reference_code}')",
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
@@ -351,7 +324,7 @@ class CalculationFormulaValidator(Validator):
|
||||
if undefined:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=_("Formula references undefined codes: {0}").format(", ".join(undefined)),
|
||||
message=f"Formula references undefined codes: {', '.join(undefined)}",
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
@@ -361,7 +334,7 @@ class CalculationFormulaValidator(Validator):
|
||||
if eval_error:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=_("Formula evaluation error: {0}").format(eval_error),
|
||||
message=f"Formula evaluation error: {eval_error}",
|
||||
row_idx=row.idx,
|
||||
)
|
||||
)
|
||||
@@ -398,7 +371,7 @@ class CalculationFormulaValidator(Validator):
|
||||
result = frappe.safe_eval(formula, eval_globals=None, eval_locals=context)
|
||||
|
||||
if not isinstance(result, (int, float)): # noqa: UP038
|
||||
return _("Formula must return a numeric value, got {0}").format(type(result).__name__)
|
||||
return f"Formula must return a numeric value, got {type(result).__name__}"
|
||||
|
||||
return None
|
||||
except Exception as e:
|
||||
@@ -421,10 +394,9 @@ class AccountFilterValidator(Validator):
|
||||
if not row.calculation_formula:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=_("{0} is required for Account Data").format(
|
||||
get_formula_field_label(row.data_source)
|
||||
),
|
||||
message="Account filter is required for Account Data",
|
||||
row_idx=row.idx,
|
||||
field="Formula",
|
||||
)
|
||||
)
|
||||
return result
|
||||
@@ -440,18 +412,18 @@ class AccountFilterValidator(Validator):
|
||||
if error:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=_("{0}: {1}").format(get_formula_field_label(row.data_source), error),
|
||||
message=error,
|
||||
row_idx=row.idx,
|
||||
field="Account Filter",
|
||||
)
|
||||
)
|
||||
|
||||
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=f"Invalid JSON format: {e!s}",
|
||||
row_idx=row.idx,
|
||||
field="Account Filter",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -466,36 +438,36 @@ class AccountFilterValidator(Validator):
|
||||
# simple condition: [field, operator, value]
|
||||
if isinstance(filter_config, list):
|
||||
if len(filter_config) != 3:
|
||||
return _("Filter must be [field, operator, value]")
|
||||
return "Filter must be [field, operator, value]"
|
||||
|
||||
field, operator, value = filter_config
|
||||
|
||||
if not isinstance(field, str) or not isinstance(operator, str):
|
||||
return _("Field and operator must be strings")
|
||||
return "Field and operator must be strings"
|
||||
|
||||
display = (field if advanced_filtering else self.account_meta.get_label(field)) or field
|
||||
|
||||
if field not in account_fields:
|
||||
return _("Field '{0}' is not a valid Account field").format(display)
|
||||
return f"Field '{display}' is not a valid Account field"
|
||||
|
||||
if operator.casefold() not in OPERATOR_MAP:
|
||||
return _("Invalid operator '{0}'").format(operator)
|
||||
return f"Invalid operator '{operator}'"
|
||||
|
||||
if operator in ["in", "not in"] and not isinstance(value, list):
|
||||
return _("Operator '{0}' requires a list value").format(operator)
|
||||
return f"Operator '{operator}' requires a list value"
|
||||
|
||||
# logical condition: {"and": [condition1, condition2]}
|
||||
elif isinstance(filter_config, dict):
|
||||
if len(filter_config) != 1:
|
||||
return _("Logical condition must have exactly one operator")
|
||||
return "Logical condition must have exactly one operator"
|
||||
|
||||
op = next(iter(filter_config.keys())).lower()
|
||||
if op not in ["and", "or"]:
|
||||
return _("Logical operators must be 'and' or 'or'")
|
||||
return "Logical operators must be 'and' or 'or'"
|
||||
|
||||
conditions = filter_config[next(iter(filter_config.keys()))]
|
||||
if not isinstance(conditions, list) or len(conditions) < 1:
|
||||
return _("Logical conditions need at least 1 sub-condition")
|
||||
return "Logical conditions need at least 1 sub-condition"
|
||||
|
||||
# recursive
|
||||
for condition in conditions:
|
||||
@@ -503,7 +475,7 @@ class AccountFilterValidator(Validator):
|
||||
if error:
|
||||
return error
|
||||
else:
|
||||
return _("Filter must be a list or dict")
|
||||
return "Filter must be a list or dict"
|
||||
|
||||
return None
|
||||
|
||||
@@ -539,31 +511,34 @@ class FormulaValidator(Validator):
|
||||
if "." not in api_path:
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=_("{0} should be in format: app.module.method").format(
|
||||
get_formula_field_label(row.data_source)
|
||||
),
|
||||
message="Custom API path should be in format: app.module.method",
|
||||
row_idx=row.idx,
|
||||
field="Formula",
|
||||
)
|
||||
)
|
||||
return result
|
||||
|
||||
# Method exists?
|
||||
try:
|
||||
get_valid_api_method(api_path)
|
||||
module_path, method_name = api_path.rsplit(".", 1)
|
||||
module = frappe.get_module(module_path)
|
||||
|
||||
if not hasattr(module, method_name):
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=f"Method '{method_name}' not found in module '{module_path}' (might be environment-specific)",
|
||||
row_idx=row.idx,
|
||||
field="Formula",
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
if isinstance(e, frappe.PermissionError | frappe.ValidationError):
|
||||
# frappe.throw inside get_valid_api_method logs a message that would pop up in UI
|
||||
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
|
||||
result.add_error(
|
||||
ValidationIssue(
|
||||
message=f"Could not validate API path: {e!s}",
|
||||
row_idx=row.idx,
|
||||
field="Formula",
|
||||
)
|
||||
else:
|
||||
message = _("Could not validate {0}: {1}").format(
|
||||
get_formula_field_label(row.data_source), str(e)
|
||||
)
|
||||
|
||||
result.add_error(ValidationIssue(message=message, row_idx=row.idx))
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -2,12 +2,7 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import whitelist_for_tests
|
||||
|
||||
from erpnext.accounts.doctype.financial_report_template.financial_report_validation import (
|
||||
FormulaValidator,
|
||||
get_valid_api_method,
|
||||
)
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
@@ -77,90 +72,3 @@ class FinancialReportTemplateTestCase(ERPNextTestSuite):
|
||||
{"doctype": "Financial Report Template", "template_name": template_name, "rows": rows_data}
|
||||
)
|
||||
return template
|
||||
|
||||
|
||||
def not_whitelisted_method(**kwargs):
|
||||
return [42.0]
|
||||
|
||||
|
||||
@whitelist_for_tests(methods=["POST"])
|
||||
def whitelisted_post_only_method(**kwargs):
|
||||
return [42.0]
|
||||
|
||||
|
||||
@whitelist_for_tests(methods=["GET"])
|
||||
def whitelisted_get_method(**kwargs):
|
||||
return [42.0]
|
||||
|
||||
|
||||
class TestCustomAPIValidation(FinancialReportTemplateTestCase):
|
||||
"""Custom API rows must point to whitelisted methods that permit GET"""
|
||||
|
||||
TEST_MODULE = "erpnext.accounts.doctype.financial_report_template.test_financial_report_template"
|
||||
NOT_WHITELISTED = f"{TEST_MODULE}.not_whitelisted_method"
|
||||
WHITELISTED_POST_ONLY = f"{TEST_MODULE}.whitelisted_post_only_method"
|
||||
WHITELISTED_GET = f"{TEST_MODULE}.whitelisted_get_method"
|
||||
|
||||
def create_api_template(self, api_path):
|
||||
template = self.create_test_template_with_rows(
|
||||
[
|
||||
{
|
||||
"reference_code": "API001",
|
||||
"display_name": "API Row",
|
||||
"data_source": "Custom API",
|
||||
"calculation_formula": api_path,
|
||||
}
|
||||
]
|
||||
)
|
||||
template.report_type = "Profit and Loss Statement"
|
||||
return template
|
||||
|
||||
def test_get_valid_api_method(self):
|
||||
self.assertRaises(frappe.PermissionError, get_valid_api_method, self.NOT_WHITELISTED)
|
||||
self.assertRaises(frappe.PermissionError, get_valid_api_method, self.WHITELISTED_POST_ONLY)
|
||||
self.assertEqual(get_valid_api_method(self.WHITELISTED_GET), frappe.get_attr(self.WHITELISTED_GET))
|
||||
|
||||
def test_save_rejects_invalid_api_methods(self):
|
||||
for api_path in (self.NOT_WHITELISTED, self.WHITELISTED_POST_ONLY):
|
||||
template = self.create_api_template(api_path)
|
||||
self.assertRaises(frappe.ValidationError, template.insert)
|
||||
|
||||
def test_save_allows_get_whitelisted_method(self):
|
||||
template = self.create_api_template(self.WHITELISTED_GET)
|
||||
template.insert()
|
||||
template.delete()
|
||||
|
||||
def test_engine_rejects_invalid_api_methods(self):
|
||||
from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
|
||||
ReportContext,
|
||||
RowProcessor,
|
||||
)
|
||||
|
||||
for api_path in (self.NOT_WHITELISTED, self.WHITELISTED_POST_ONLY):
|
||||
template = self.create_api_template(api_path)
|
||||
context = ReportContext(template=template, filters={}, period_list=[{"key": "p1"}])
|
||||
processor = RowProcessor(context)
|
||||
self.assertRaises(frappe.PermissionError, processor._process_api_row, template.rows[0])
|
||||
|
||||
def test_engine_calls_valid_api_method(self):
|
||||
from erpnext.accounts.doctype.financial_report_template.financial_report_engine import (
|
||||
ReportContext,
|
||||
RowProcessor,
|
||||
)
|
||||
|
||||
template = self.create_api_template(self.WHITELISTED_GET)
|
||||
context = ReportContext(template=template, filters={}, period_list=[{"key": "p1"}])
|
||||
processor = RowProcessor(context)
|
||||
row_data = processor._process_api_row(template.rows[0])
|
||||
self.assertEqual(row_data.values, [42.0])
|
||||
|
||||
def test_validation_keeps_message_log_clean(self):
|
||||
validator = FormulaValidator(frappe._dict(rows=[]))
|
||||
message_count = len(frappe.local.message_log)
|
||||
|
||||
# last path raises AppNotInstalledError, which also logs a message via frappe.throw
|
||||
for api_path in (self.NOT_WHITELISTED, self.WHITELISTED_POST_ONLY, "missing_app.api.method"):
|
||||
row = frappe._dict(data_source="Custom API", calculation_formula=api_path, idx=1)
|
||||
result = validator.validate(row)
|
||||
self.assertFalse(result.is_valid)
|
||||
self.assertEqual(len(frappe.local.message_log), message_count)
|
||||
|
||||
@@ -1402,7 +1402,6 @@ def get_payment_entry_against_order(
|
||||
dt, dn, amount=None, debit_in_account_currency=None, journal_entry=False, bank_account=None
|
||||
):
|
||||
ref_doc = frappe.get_doc(dt, dn)
|
||||
ref_doc.check_permission()
|
||||
|
||||
if flt(ref_doc.per_billed, 2) > 0:
|
||||
frappe.throw(_("Can only make payment against unbilled {0}").format(dt))
|
||||
@@ -1448,8 +1447,6 @@ def get_payment_entry_against_invoice(
|
||||
dt, dn, amount=None, debit_in_account_currency=None, journal_entry=False, bank_account=None
|
||||
):
|
||||
ref_doc = frappe.get_doc(dt, dn)
|
||||
ref_doc.check_permission()
|
||||
|
||||
if dt == "Sales Invoice":
|
||||
party_type = "Customer"
|
||||
party_account = get_party_account_based_on_invoice_discounting(dn) or ref_doc.debit_to
|
||||
@@ -1485,8 +1482,6 @@ def get_payment_entry_against_invoice(
|
||||
|
||||
|
||||
def get_payment_entry(ref_doc, args):
|
||||
frappe.has_permission("Journal Entry", ptype="create", throw=True)
|
||||
|
||||
cost_center = ref_doc.get("cost_center") or frappe.get_cached_value(
|
||||
"Company", ref_doc.company, "cost_center"
|
||||
)
|
||||
|
||||
@@ -56,9 +56,7 @@ class LedgerMerge(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def form_start_merge(docname):
|
||||
lm_doc = frappe.get_doc("Ledger Merge", docname)
|
||||
lm_doc.check_permission("write")
|
||||
return lm_doc.start_merge()
|
||||
return frappe.get_doc("Ledger Merge", docname).start_merge()
|
||||
|
||||
|
||||
def start_merge(docname):
|
||||
|
||||
@@ -297,9 +297,6 @@ def start_import(invoices):
|
||||
invoice_number = d.invoice_number
|
||||
doc = frappe.get_doc(d)
|
||||
doc.flags.ignore_mandatory = True
|
||||
# the outstanding amount is entered inclusive of tax, so taxes must not
|
||||
# be added on top of it
|
||||
doc.flags.dont_auto_add_taxes = True
|
||||
doc.insert(set_name=invoice_number)
|
||||
doc.submit()
|
||||
if not frappe.in_test:
|
||||
|
||||
@@ -4,11 +4,9 @@
|
||||
import frappe
|
||||
from frappe.utils import add_days, today
|
||||
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import (
|
||||
get_temporary_opening_account,
|
||||
)
|
||||
from erpnext.accounts.doctype.tax_rule.test_tax_rule import make_tax_rule
|
||||
from erpnext.projects.doctype.project.test_project import make_project
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
@@ -128,55 +126,6 @@ class TestOpeningInvoiceCreationTool(ERPNextTestSuite):
|
||||
for invoice in invoices:
|
||||
self.assertEqual(frappe.db.get_value("Sales Invoice", invoice, "department"), "Sales - _TOIC")
|
||||
|
||||
@ERPNextTestSuite.change_settings(
|
||||
"Accounts Settings",
|
||||
{"add_taxes_from_taxes_and_charges_template": 1, "add_taxes_from_item_tax_template": 0},
|
||||
)
|
||||
def test_opening_invoice_creation_without_taxes(self):
|
||||
company = "_Test Opening Invoice Company"
|
||||
template = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Sales Taxes and Charges Template",
|
||||
"company": company,
|
||||
"title": "_Test Opening Invoice Tax",
|
||||
"taxes": [
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": create_account(
|
||||
account_name="_Test Opening Tax Account",
|
||||
parent_account="Duties and Taxes - _TOIC",
|
||||
account_type="Tax",
|
||||
company=company,
|
||||
),
|
||||
"description": "Test taxes",
|
||||
"rate": 9,
|
||||
}
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
# makes the template the default for the party, as it would be on a live site
|
||||
make_tax_rule(tax_type="Sales", company=company, sales_tax_template=template.name, save=1)
|
||||
|
||||
tool = self.make_invoices(company=company, return_doc=True)
|
||||
invoices = tool.make_invoices()
|
||||
self.assertEqual(len(invoices), 2)
|
||||
|
||||
# outstanding amount is entered inclusive of tax, so taxes must not be added on top of it
|
||||
for invoice in invoices:
|
||||
si = frappe.get_doc("Sales Invoice", invoice)
|
||||
self.assertFalse(si.taxes)
|
||||
self.assertEqual(si.grand_total, 200)
|
||||
self.assertEqual(si.outstanding_amount, 200)
|
||||
|
||||
# the same invoice created outside the tool keeps the default taxes,
|
||||
# since adding them there is the user's decision
|
||||
si = frappe.get_doc(tool.get_invoices()[0])
|
||||
si.flags.ignore_mandatory = True
|
||||
si.insert()
|
||||
self.assertTrue(si.taxes)
|
||||
self.assertEqual(si.grand_total, 218)
|
||||
|
||||
def test_opening_entry_project_linking(self):
|
||||
doc = self.make_invoices(
|
||||
company="_Test Opening Invoice Company", invoice_type="Sales", return_doc=True
|
||||
|
||||
@@ -2893,11 +2893,7 @@ def get_payment_entry(
|
||||
reference_date=None,
|
||||
created_from_payment_request=False,
|
||||
):
|
||||
frappe.has_permission("Payment Entry", ptype="create", throw=True)
|
||||
|
||||
doc = frappe.get_doc(dt, dn)
|
||||
doc.check_permission()
|
||||
|
||||
over_billing_allowance = frappe.get_single_value("Accounts Settings", "over_billing_allowance")
|
||||
if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= (100.0 + over_billing_allowance):
|
||||
frappe.throw(_("Can only make payment against unbilled {0}").format(_(dt)))
|
||||
|
||||
@@ -83,7 +83,6 @@ def get_supplier_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
@frappe.whitelist()
|
||||
def make_payment_records(name, supplier, mode_of_payment=None):
|
||||
doc = frappe.get_doc("Payment Order", name)
|
||||
doc.check_permission()
|
||||
make_journal_entry(doc, supplier, mode_of_payment)
|
||||
|
||||
|
||||
|
||||
@@ -92,7 +92,6 @@ frappe.ui.form.on("Payment Request", "is_a_subscription", function (frm) {
|
||||
freeze: true,
|
||||
callback: function (data) {
|
||||
if (!data.exc) {
|
||||
frm.clear_table("subscription_plans");
|
||||
$.each(data.message || [], function (i, v) {
|
||||
var d = frappe.model.add_child(
|
||||
frm.doc,
|
||||
|
||||
@@ -1188,7 +1188,6 @@ def get_irequests_of_payment_request(doc: str | None = None) -> list:
|
||||
@frappe.whitelist()
|
||||
def get_available_payment_schedules(reference_doctype, reference_name):
|
||||
ref_doc = frappe.get_doc(reference_doctype, reference_name)
|
||||
ref_doc.check_permission()
|
||||
|
||||
if not hasattr(ref_doc, "payment_schedule") or not ref_doc.payment_schedule:
|
||||
return []
|
||||
|
||||
@@ -261,15 +261,12 @@ def get_cashiers(doctype, txt, searchfield, start, page_len, filters):
|
||||
@frappe.whitelist()
|
||||
def get_invoices(start, end, pos_profile, user):
|
||||
invoice_doctype = frappe.db.get_single_value("POS Settings", "invoice_type")
|
||||
frappe.has_permission("POS Profile", doc=pos_profile, throw=True)
|
||||
|
||||
frappe.has_permission("Sales Invoice", throw=True)
|
||||
sales_inv_query = build_invoice_query("Sales Invoice", user, pos_profile, start, end)
|
||||
|
||||
query = sales_inv_query
|
||||
|
||||
if invoice_doctype == "POS Invoice":
|
||||
frappe.has_permission("POS Invoice", throw=True)
|
||||
pos_inv_query = build_invoice_query("POS Invoice", user, pos_profile, start, end)
|
||||
query = query + pos_inv_query
|
||||
|
||||
|
||||
@@ -280,40 +280,40 @@ def pos_profile_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
user = frappe.session["user"]
|
||||
company = filters.get("company") or frappe.defaults.get_user_default("company")
|
||||
|
||||
allowed_pos_profiles = frappe.get_list("POS Profile", pluck="name")
|
||||
args = {
|
||||
"user": user,
|
||||
"start": start,
|
||||
"company": company,
|
||||
"page_len": page_len,
|
||||
"txt": "%%%s%%" % txt,
|
||||
}
|
||||
|
||||
if not allowed_pos_profiles:
|
||||
return {}
|
||||
|
||||
pf = frappe.qb.DocType("POS Profile")
|
||||
pfu = frappe.qb.DocType("POS Profile User")
|
||||
|
||||
pos_profile = (
|
||||
frappe.qb.from_(pf)
|
||||
.inner_join(pfu)
|
||||
.on(pfu.parent == pf.name)
|
||||
.select(pf.name)
|
||||
.where((pfu.user == user) & (pf.company == company) & pf.name.like(f"%{txt}%") & (pf.disabled == 0))
|
||||
.where(pf.name.isin(allowed_pos_profiles))
|
||||
.limit(page_len)
|
||||
.offset(start)
|
||||
.run()
|
||||
pos_profile = frappe.db.sql(
|
||||
"""select pf.name
|
||||
from
|
||||
`tabPOS Profile` pf, `tabPOS Profile User` pfu
|
||||
where
|
||||
pfu.parent = pf.name and pfu.user = %(user)s and pf.company = %(company)s
|
||||
and (pf.name like %(txt)s)
|
||||
and pf.disabled = 0 limit %(page_len)s offset %(start)s""",
|
||||
args,
|
||||
)
|
||||
|
||||
if not pos_profile:
|
||||
pos_profile = (
|
||||
frappe.qb.from_(pf)
|
||||
.left_join(pfu)
|
||||
.on(pf.name == pfu.parent)
|
||||
.select(pf.name)
|
||||
.where(
|
||||
(pfu.user.isnull() | (pfu.user == ""))
|
||||
& (pf.company == company)
|
||||
& pf.name.like(f"%{txt}%")
|
||||
& (pf.disabled == 0)
|
||||
& (pf.name.isin(allowed_pos_profiles))
|
||||
)
|
||||
.run()
|
||||
del args["user"]
|
||||
|
||||
pos_profile = frappe.db.sql(
|
||||
"""select pf.name
|
||||
from
|
||||
`tabPOS Profile` pf left join `tabPOS Profile User` pfu
|
||||
on
|
||||
pf.name = pfu.parent
|
||||
where
|
||||
ifnull(pfu.user, '') = ''
|
||||
and pf.company = %(company)s
|
||||
and pf.name like %(txt)s
|
||||
and pf.disabled = 0""",
|
||||
args,
|
||||
)
|
||||
|
||||
return pos_profile
|
||||
|
||||
@@ -12,9 +12,8 @@
|
||||
{
|
||||
"fieldname": "fieldname",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Fieldname",
|
||||
"read_only": 1
|
||||
"hidden": 1,
|
||||
"label": "Fieldname"
|
||||
},
|
||||
{
|
||||
"fieldname": "field",
|
||||
@@ -27,7 +26,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-08-31 20:41:12.000000",
|
||||
"modified": "2025-07-29 18:08:40.323579",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Search Fields",
|
||||
|
||||
@@ -1,9 +1,40 @@
|
||||
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
function is_valid_invoice_field(df) {
|
||||
return frappe.model.no_value_type.indexOf(df.fieldtype) === -1 || df.fieldtype === "Button";
|
||||
}
|
||||
let search_fields_datatypes = [
|
||||
"Data",
|
||||
"Link",
|
||||
"Dynamic Link",
|
||||
"Long Text",
|
||||
"Select",
|
||||
"Small Text",
|
||||
"Text",
|
||||
"Text Editor",
|
||||
];
|
||||
let do_not_include_fields = [
|
||||
"naming_series",
|
||||
"item_code",
|
||||
"item_name",
|
||||
"stock_uom",
|
||||
"asset_naming_series",
|
||||
"default_material_request_type",
|
||||
"valuation_method",
|
||||
"warranty_period",
|
||||
"weight_uom",
|
||||
"batch_number_series",
|
||||
"serial_no_series",
|
||||
"purchase_uom",
|
||||
"customs_tariff_number",
|
||||
"sales_uom",
|
||||
"deferred_revenue_account",
|
||||
"deferred_expense_account",
|
||||
"quality_inspection_template",
|
||||
"route",
|
||||
"slideshow",
|
||||
"website_image_alt",
|
||||
"thumbnail",
|
||||
"web_long_description",
|
||||
];
|
||||
|
||||
frappe.ui.form.on("POS Settings", {
|
||||
onload: function (frm) {
|
||||
@@ -11,97 +42,74 @@ frappe.ui.form.on("POS Settings", {
|
||||
frm.trigger("add_search_options");
|
||||
},
|
||||
|
||||
invoice_type: function (frm) {
|
||||
frm.trigger("get_invoice_fields");
|
||||
},
|
||||
|
||||
get_invoice_fields: function (frm) {
|
||||
const invoice_type = frm.doc.invoice_type;
|
||||
if (!invoice_type) return;
|
||||
|
||||
frappe.model.with_doctype(invoice_type, () => {
|
||||
// the invoice type can change again while the meta loads
|
||||
if (frm.doc.invoice_type !== invoice_type) return;
|
||||
|
||||
const fields = frappe.get_doc("DocType", invoice_type).fields.filter(is_valid_invoice_field);
|
||||
frappe.model.with_doctype("POS Invoice", () => {
|
||||
var fields = $.map(frappe.get_doc("DocType", "POS Invoice").fields, function (d) {
|
||||
if (
|
||||
frappe.model.no_value_type.indexOf(d.fieldtype) === -1 ||
|
||||
["Button"].includes(d.fieldtype)
|
||||
) {
|
||||
return { label: d.label + " (" + d.fieldtype + ")", value: d.fieldname };
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
frm.fields_dict.invoice_fields.grid.update_docfield_property(
|
||||
"fieldname",
|
||||
"options",
|
||||
[""].concat(
|
||||
fields.map((df) => {
|
||||
return { label: `${df.label} (${df.fieldtype})`, value: df.fieldname };
|
||||
})
|
||||
)
|
||||
[""].concat(fields)
|
||||
);
|
||||
|
||||
frm.trigger("validate_invoice_fields");
|
||||
});
|
||||
},
|
||||
|
||||
validate_invoice_fields: function (frm) {
|
||||
const valid_fieldnames = frappe
|
||||
.get_doc("DocType", frm.doc.invoice_type)
|
||||
.fields.filter(is_valid_invoice_field)
|
||||
.map((df) => df.fieldname);
|
||||
|
||||
const invalid_fields = (frm.doc.invoice_fields || [])
|
||||
.filter((row) => row.fieldname && !valid_fieldnames.includes(row.fieldname))
|
||||
.map((row) => `#${row.idx} ${row.fieldname}`);
|
||||
|
||||
if (!invalid_fields.length) return;
|
||||
|
||||
frappe.msgprint({
|
||||
title: __("Invalid POS Fields"),
|
||||
indicator: "orange",
|
||||
message: __("The following rows are not valid fields of {0} and have to be removed: {1}", [
|
||||
frm.doc.invoice_type.bold(),
|
||||
invalid_fields.join(", "),
|
||||
]),
|
||||
});
|
||||
},
|
||||
|
||||
add_search_options: function (frm) {
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.pos_settings.pos_settings.get_pos_search_field_options",
|
||||
callback: ({ message }) => {
|
||||
const fields = message || [];
|
||||
frappe.model.with_doctype("Item", () => {
|
||||
var fields = $.map(frappe.get_doc("DocType", "Item").fields, function (d) {
|
||||
if (
|
||||
search_fields_datatypes.includes(d.fieldtype) &&
|
||||
!do_not_include_fields.includes(d.fieldname)
|
||||
) {
|
||||
return [d.label];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
frm.searchable_item_fields = Object.fromEntries(
|
||||
fields.map((df) => [df.option, df.fieldname])
|
||||
);
|
||||
|
||||
frm.fields_dict.pos_search_fields.grid.update_docfield_property(
|
||||
"field",
|
||||
"options",
|
||||
[""].concat(fields.map((df) => df.option))
|
||||
);
|
||||
},
|
||||
fields.unshift("");
|
||||
frm.fields_dict.pos_search_fields.grid.update_docfield_property("field", "options", fields);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("POS Search Fields", {
|
||||
field: function (frm, doctype, name) {
|
||||
const doc = frappe.get_doc(doctype, name);
|
||||
var doc = frappe.get_doc(doctype, name);
|
||||
var df = $.map(frappe.get_doc("DocType", "Item").fields, function (d) {
|
||||
if (doc.field == d.label && search_fields_datatypes.includes(d.fieldtype)) {
|
||||
return d;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})[0];
|
||||
|
||||
doc.fieldname = frm.searchable_item_fields?.[doc.field] || "";
|
||||
frm.refresh_field("pos_search_fields");
|
||||
doc.fieldname = df.fieldname;
|
||||
frm.refresh_field("fields");
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("POS Field", {
|
||||
fieldname: function (frm, doctype, name) {
|
||||
const doc = frappe.get_doc(doctype, name);
|
||||
const invoice_meta = frappe.get_doc("DocType", frm.doc.invoice_type);
|
||||
const df = invoice_meta?.fields.find((d) => d.fieldname === doc.fieldname);
|
||||
if (!df) return;
|
||||
var doc = frappe.get_doc(doctype, name);
|
||||
var df = $.map(frappe.get_doc("DocType", "POS Invoice").fields, function (d) {
|
||||
return doc.fieldname == d.fieldname ? d : null;
|
||||
})[0];
|
||||
|
||||
doc.label = df.label;
|
||||
doc.reqd = df.reqd;
|
||||
doc.options = df.options;
|
||||
doc.fieldtype = df.fieldtype;
|
||||
doc.default_value = df.default;
|
||||
frm.refresh_field("invoice_fields");
|
||||
frm.refresh_field("fields");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,46 +5,8 @@ from collections import Counter
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model import no_value_fields
|
||||
from frappe.model.document import Document
|
||||
|
||||
SEARCH_FIELD_TYPES = (
|
||||
"Data",
|
||||
"Link",
|
||||
"Dynamic Link",
|
||||
"Long Text",
|
||||
"Select",
|
||||
"Small Text",
|
||||
"Text",
|
||||
"Text Editor",
|
||||
)
|
||||
|
||||
# Item fields that are of a searchable fieldtype, but are not meaningful to search a POS item by
|
||||
DO_NOT_INCLUDE_FIELDS = (
|
||||
"naming_series",
|
||||
"item_code",
|
||||
"item_name",
|
||||
"stock_uom",
|
||||
"asset_naming_series",
|
||||
"default_material_request_type",
|
||||
"valuation_method",
|
||||
"warranty_period",
|
||||
"weight_uom",
|
||||
"batch_number_series",
|
||||
"serial_no_series",
|
||||
"purchase_uom",
|
||||
"customs_tariff_number",
|
||||
"sales_uom",
|
||||
"deferred_revenue_account",
|
||||
"deferred_expense_account",
|
||||
"quality_inspection_template",
|
||||
"route",
|
||||
"slideshow",
|
||||
"website_image_alt",
|
||||
"thumbnail",
|
||||
"web_long_description",
|
||||
)
|
||||
|
||||
|
||||
class POSSettings(Document):
|
||||
# begin: auto-generated types
|
||||
@@ -70,10 +32,17 @@ class POSSettings(Document):
|
||||
if old_doc.invoice_type != self.invoice_type:
|
||||
self.validate_invoice_type()
|
||||
|
||||
self.validate_duplicate_invoice_fields()
|
||||
self.validate_invoice_fields()
|
||||
self.validate_duplicate_pos_search_fields()
|
||||
self.validate_pos_search_fields()
|
||||
|
||||
def validate_invoice_fields(self):
|
||||
invoice_fields = [field.fieldname for field in self.invoice_fields]
|
||||
duplicate_invoice_fields = {key for key, value in Counter(invoice_fields).items() if value > 1}
|
||||
|
||||
if len(duplicate_invoice_fields):
|
||||
for field in duplicate_invoice_fields:
|
||||
frappe.throw(
|
||||
title=_("Duplicate POS Fields"), msg=_("'{0}' has been already added.").format(field)
|
||||
)
|
||||
|
||||
def validate_invoice_type(self):
|
||||
pos_opening_entries_count = frappe.db.count(
|
||||
@@ -86,94 +55,3 @@ class POSSettings(Document):
|
||||
),
|
||||
title=_("Invoice Document Type Selection Error"),
|
||||
)
|
||||
|
||||
def validate_duplicate_invoice_fields(self):
|
||||
invoice_fields = [field.fieldname for field in self.invoice_fields]
|
||||
duplicate_invoice_fields = {key for key, value in Counter(invoice_fields).items() if value > 1}
|
||||
|
||||
if len(duplicate_invoice_fields):
|
||||
for field in duplicate_invoice_fields:
|
||||
frappe.throw(
|
||||
title=_("Duplicate POS Fields"), msg=_("'{0}' has been already added.").format(field)
|
||||
)
|
||||
|
||||
def validate_invoice_fields(self):
|
||||
if not self.invoice_type:
|
||||
return
|
||||
|
||||
meta = frappe.get_meta(self.invoice_type)
|
||||
|
||||
for field in self.invoice_fields:
|
||||
df = meta.get_field(field.fieldname)
|
||||
|
||||
if not df or not is_valid_invoice_field(df):
|
||||
frappe.throw(
|
||||
title=_("Invalid POS Field"),
|
||||
msg=_("Row #{0}: '{1}' is not a valid field of {2}.").format(
|
||||
field.idx, frappe.bold(field.fieldname or ""), frappe.bold(_(self.invoice_type))
|
||||
),
|
||||
)
|
||||
|
||||
# read only in the form, so keep them in sync with the invoice
|
||||
field.label = df.label
|
||||
field.fieldtype = df.fieldtype
|
||||
field.options = df.options
|
||||
|
||||
def validate_duplicate_pos_search_fields(self):
|
||||
fieldnames = [field.fieldname for field in self.pos_search_fields]
|
||||
duplicate_fieldnames = {key for key, value in Counter(fieldnames).items() if value > 1}
|
||||
|
||||
for fieldname in duplicate_fieldnames:
|
||||
frappe.throw(
|
||||
title=_("Duplicate POS Search Fields"),
|
||||
msg=_("'{0}' has been already added.").format(fieldname),
|
||||
)
|
||||
|
||||
def validate_pos_search_fields(self):
|
||||
searchable_fields = {df.fieldname: df for df in get_searchable_item_fields()}
|
||||
|
||||
for field in self.pos_search_fields:
|
||||
df = searchable_fields.get(field.fieldname)
|
||||
|
||||
if not df:
|
||||
frappe.throw(
|
||||
title=_("Invalid POS Search Field"),
|
||||
msg=_("Row #{0}: '{1}' cannot be used to search items.").format(
|
||||
field.idx, frappe.bold(field.fieldname or "")
|
||||
),
|
||||
)
|
||||
|
||||
if field.field != get_search_field_option(df):
|
||||
frappe.throw(
|
||||
title=_("Invalid POS Search Field"),
|
||||
msg=_("Row #{0}: '{1}' does not match {2}.").format(
|
||||
field.idx, frappe.bold(field.field or ""), frappe.bold(df.fieldname)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def is_valid_invoice_field(df):
|
||||
return df.fieldtype not in no_value_fields or df.fieldtype == "Button"
|
||||
|
||||
|
||||
def get_searchable_item_fields():
|
||||
return [
|
||||
df
|
||||
for df in frappe.get_meta("Item").fields
|
||||
if df.fieldtype in SEARCH_FIELD_TYPES and df.fieldname not in DO_NOT_INCLUDE_FIELDS
|
||||
]
|
||||
|
||||
|
||||
def get_search_field_option(df):
|
||||
# the fieldname keeps the option unique, two Item fields can share a label
|
||||
return f"{df.label} ({df.fieldname})"
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_pos_search_field_options():
|
||||
frappe.has_permission("POS Settings", throw=True)
|
||||
|
||||
return [
|
||||
{"option": get_search_field_option(df), "fieldname": df.fieldname}
|
||||
for df in get_searchable_item_fields()
|
||||
]
|
||||
|
||||
@@ -2,135 +2,8 @@
|
||||
# See license.txt
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.patches.v16_0.append_fieldname_to_pos_search_fields import execute as append_fieldname
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestPOSSettings(ERPNextTestSuite):
|
||||
def setUp(self):
|
||||
self.settings = frappe.get_single("POS Settings")
|
||||
self.settings.invoice_fields = []
|
||||
self.settings.pos_search_fields = []
|
||||
|
||||
def assertInvalid(self, message):
|
||||
with self.assertRaises(frappe.ValidationError) as context:
|
||||
self.settings.save()
|
||||
|
||||
self.assertIn(message, str(context.exception))
|
||||
|
||||
def test_invoice_field_is_validated_against_invoice_type(self):
|
||||
# consolidated_invoice exists on POS Invoice only
|
||||
self.settings.invoice_type = "POS Invoice"
|
||||
self.settings.append("invoice_fields", {"fieldname": "consolidated_invoice"})
|
||||
self.settings.save()
|
||||
|
||||
self.settings.invoice_type = "Sales Invoice"
|
||||
self.assertInvalid("is not a valid field of")
|
||||
|
||||
def test_field_common_to_both_invoice_types_is_allowed(self):
|
||||
for invoice_type in ("POS Invoice", "Sales Invoice"):
|
||||
self.settings.invoice_type = invoice_type
|
||||
self.settings.invoice_fields = []
|
||||
self.settings.append("invoice_fields", {"fieldname": "po_no"})
|
||||
self.settings.save()
|
||||
|
||||
def test_unknown_invoice_field_is_not_allowed(self):
|
||||
self.settings.append("invoice_fields", {"fieldname": "not_a_field"})
|
||||
self.assertInvalid("is not a valid field of")
|
||||
|
||||
def test_layout_invoice_field_is_not_allowed(self):
|
||||
self.settings.append("invoice_fields", {"fieldname": "accounting_dimensions_section"})
|
||||
self.assertInvalid("is not a valid field of")
|
||||
|
||||
def test_invoice_field_properties_are_set_from_the_invoice(self):
|
||||
self.settings.append(
|
||||
"invoice_fields", {"fieldname": "customer", "label": "Tampered", "fieldtype": "Data"}
|
||||
)
|
||||
self.settings.save()
|
||||
|
||||
field = self.settings.invoice_fields[0]
|
||||
self.assertEqual(field.label, "Customer")
|
||||
self.assertEqual(field.fieldtype, "Link")
|
||||
self.assertEqual(field.options, "Customer")
|
||||
|
||||
def test_searchable_item_field_is_allowed(self):
|
||||
self.settings.append(
|
||||
"pos_search_fields", {"field": "Description (description)", "fieldname": "description"}
|
||||
)
|
||||
self.settings.save()
|
||||
|
||||
self.assertEqual(self.settings.pos_search_fields[0].fieldname, "description")
|
||||
|
||||
def test_excluded_search_field_is_not_allowed(self):
|
||||
self.settings.append(
|
||||
"pos_search_fields", {"field": "Item Name (item_name)", "fieldname": "item_name"}
|
||||
)
|
||||
self.assertInvalid("cannot be used to search items")
|
||||
|
||||
def test_search_field_of_unsearchable_type_is_not_allowed(self):
|
||||
# maintain stock is a Check field
|
||||
self.settings.append(
|
||||
"pos_search_fields", {"field": "Maintain Stock (is_stock_item)", "fieldname": "is_stock_item"}
|
||||
)
|
||||
self.assertInvalid("cannot be used to search items")
|
||||
|
||||
def test_unknown_search_field_is_not_allowed(self):
|
||||
self.settings.append(
|
||||
"pos_search_fields", {"field": "Nope (not_an_item_field)", "fieldname": "not_an_item_field"}
|
||||
)
|
||||
self.assertInvalid("cannot be used to search items")
|
||||
|
||||
def test_search_field_without_a_fieldname_is_not_allowed(self):
|
||||
# the form fills the fieldname in, it cannot be picked on its own
|
||||
self.settings.append("pos_search_fields", {"field": "Description (description)"})
|
||||
self.assertInvalid("cannot be used to search items")
|
||||
|
||||
def test_search_field_option_must_match_its_fieldname(self):
|
||||
self.settings.append("pos_search_fields", {"field": "Brand (brand)", "fieldname": "description"})
|
||||
self.assertInvalid("does not match")
|
||||
|
||||
def test_bare_label_is_not_accepted_as_a_search_field(self):
|
||||
# the stored option carries the fieldname, the patch backfills older rows
|
||||
self.settings.append("pos_search_fields", {"field": "Description", "fieldname": "description"})
|
||||
self.assertInvalid("does not match")
|
||||
|
||||
def test_duplicate_search_fields_are_not_allowed(self):
|
||||
for _ in range(2):
|
||||
self.settings.append(
|
||||
"pos_search_fields", {"field": "Description (description)", "fieldname": "description"}
|
||||
)
|
||||
|
||||
self.assertInvalid("has been already added")
|
||||
|
||||
def test_patch_appends_the_fieldname_to_a_legacy_search_field(self):
|
||||
self.settings.append(
|
||||
"pos_search_fields", {"field": "Description (description)", "fieldname": "description"}
|
||||
)
|
||||
self.settings.save()
|
||||
|
||||
row = self.settings.pos_search_fields[0].name
|
||||
frappe.db.set_value("POS Search Fields", row, "field", "Description", update_modified=False)
|
||||
|
||||
append_fieldname()
|
||||
|
||||
self.assertEqual(frappe.db.get_value("POS Search Fields", row, "field"), "Description (description)")
|
||||
|
||||
def test_patch_leaves_an_already_migrated_search_field_alone(self):
|
||||
self.settings.append(
|
||||
"pos_search_fields", {"field": "Description (description)", "fieldname": "description"}
|
||||
)
|
||||
self.settings.save()
|
||||
|
||||
append_fieldname()
|
||||
|
||||
row = self.settings.pos_search_fields[0].name
|
||||
self.assertEqual(frappe.db.get_value("POS Search Fields", row, "field"), "Description (description)")
|
||||
|
||||
def test_invoice_fields_are_skipped_when_no_invoice_type_is_selected(self):
|
||||
self.settings.invoice_type = None
|
||||
self.settings.append("invoice_fields", {"fieldname": "customer"})
|
||||
self.settings.save()
|
||||
|
||||
self.assertEqual(self.settings.invoice_fields[0].fieldname, "customer")
|
||||
pass
|
||||
|
||||
@@ -142,8 +142,6 @@ def start_pcv_processing(docname: str):
|
||||
|
||||
@frappe.whitelist()
|
||||
def pause_pcv_processing(docname: str):
|
||||
frappe.has_permission("Process Period Closing Voucher", ptype="write", doc=docname, throw=True)
|
||||
|
||||
ppcv = qb.DocType("Process Period Closing Voucher")
|
||||
qb.update(ppcv).set(ppcv.status, "Paused").where(ppcv.name.eq(docname)).run()
|
||||
|
||||
@@ -159,8 +157,6 @@ def pause_pcv_processing(docname: str):
|
||||
|
||||
@frappe.whitelist()
|
||||
def cancel_pcv_processing(docname: str):
|
||||
frappe.has_permission("Process Period Closing Voucher", ptype="cancel", doc=docname, throw=True)
|
||||
|
||||
ppcv = qb.DocType("Process Period Closing Voucher")
|
||||
qb.update(ppcv).set(ppcv.status, "Cancelled").where(ppcv.name.eq(docname)).run()
|
||||
|
||||
@@ -175,8 +171,6 @@ def cancel_pcv_processing(docname: str):
|
||||
|
||||
@frappe.whitelist()
|
||||
def resume_pcv_processing(docname: str):
|
||||
frappe.has_permission("Process Period Closing Voucher", ptype="write", doc=docname, throw=True)
|
||||
|
||||
ppcv = qb.DocType("Process Period Closing Voucher")
|
||||
qb.update(ppcv).set(ppcv.status, "Running").where(ppcv.name.eq(docname)).run()
|
||||
|
||||
|
||||
@@ -42,7 +42,6 @@ from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled
|
||||
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
|
||||
from erpnext.controllers.accounts_controller import merge_taxes, validate_account_head
|
||||
from erpnext.controllers.buying_controller import BuyingController
|
||||
from erpnext.controllers.mapper import get_qty_already_mapped
|
||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
|
||||
update_billed_amount_based_on_po,
|
||||
)
|
||||
@@ -2120,11 +2119,6 @@ def make_purchase_receipt(source_name, target_doc=None, args=None):
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
mapped_qty_by_item = get_qty_already_mapped(target_doc, "purchase_invoice_item")
|
||||
|
||||
def received_and_mapped_qty(obj):
|
||||
return flt(obj.received_qty) + flt(mapped_qty_by_item.get(obj.name, 0))
|
||||
|
||||
def post_parent_process(source_parent, target_parent):
|
||||
remove_items_with_zero_qty(target_parent)
|
||||
set_missing_values(source_parent, target_parent)
|
||||
@@ -2148,13 +2142,15 @@ def make_purchase_receipt(source_name, target_doc=None, args=None):
|
||||
or {}
|
||||
)
|
||||
|
||||
pending_qty = flt(obj.qty) - received_and_mapped_qty(obj)
|
||||
|
||||
target.qty = pending_qty - flt(returned_qty_map.get("qty"))
|
||||
target.received_qty = pending_qty
|
||||
target.stock_qty = (pending_qty - flt(returned_qty_map.get("qty"))) * flt(obj.conversion_factor)
|
||||
target.amount = pending_qty * flt(obj.rate)
|
||||
target.base_amount = pending_qty * flt(obj.rate) * flt(source_parent.conversion_rate)
|
||||
target.qty = flt(obj.qty) - flt(obj.received_qty) - flt(returned_qty_map.get("qty"))
|
||||
target.received_qty = flt(obj.qty) - flt(obj.received_qty)
|
||||
target.stock_qty = (flt(obj.qty) - flt(obj.received_qty) - flt(returned_qty_map.get("qty"))) * flt(
|
||||
obj.conversion_factor
|
||||
)
|
||||
target.amount = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate)
|
||||
target.base_amount = (
|
||||
(flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate) * flt(source_parent.conversion_rate)
|
||||
)
|
||||
|
||||
def select_item(d):
|
||||
filtered_items = args.get("filtered_children", [])
|
||||
@@ -2184,9 +2180,7 @@ def make_purchase_receipt(source_name, target_doc=None, args=None):
|
||||
"wip_composite_asset": "wip_composite_asset",
|
||||
},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: (
|
||||
abs(received_and_mapped_qty(doc)) < abs(doc.qty) and select_item(doc)
|
||||
),
|
||||
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty) and select_item(doc),
|
||||
},
|
||||
"Purchase Taxes and Charges": {
|
||||
"doctype": "Purchase Taxes and Charges",
|
||||
|
||||
@@ -27,7 +27,6 @@ def start_payment_ledger_repost(docname=None):
|
||||
"""
|
||||
if docname:
|
||||
repost_doc = frappe.get_doc("Repost Payment Ledger", docname)
|
||||
repost_doc.check_permission("submit")
|
||||
if repost_doc.docstatus.is_submitted() and repost_doc.repost_status in ["Queued", "Failed"]:
|
||||
try:
|
||||
for entry in repost_doc.repost_vouchers:
|
||||
|
||||
@@ -9,7 +9,6 @@ from frappe.utils import cint, flt
|
||||
|
||||
from erpnext.accounts.report.general_ledger.general_ledger import get_accounts_with_children
|
||||
from erpnext.accounts.report.trial_balance.trial_balance import validate_filters
|
||||
from erpnext.accounts.utils import get_currency_precision
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -44,7 +43,6 @@ def get_data(filters, show_party_name):
|
||||
account_filter = get_accounts_with_children(filters.get("account"))
|
||||
|
||||
company_currency = frappe.get_cached_value("Company", filters.company, "default_currency")
|
||||
precision = get_currency_precision()
|
||||
opening_balances = get_opening_balances(filters, account_filter)
|
||||
balances_within_period = get_balances_within_period(filters, account_filter)
|
||||
|
||||
@@ -67,17 +65,14 @@ def get_data(filters, show_party_name):
|
||||
|
||||
# opening
|
||||
opening_debit, opening_credit = opening_balances.get(party.name, [0, 0])
|
||||
opening_debit, opening_credit = flt(opening_debit, precision), flt(opening_credit, precision)
|
||||
row.update({"opening_debit": opening_debit, "opening_credit": opening_credit})
|
||||
|
||||
# within period
|
||||
debit, credit = balances_within_period.get(party.name, [0, 0])
|
||||
debit, credit = flt(debit, precision), flt(credit, precision)
|
||||
row.update({"debit": debit, "credit": credit})
|
||||
|
||||
# closing
|
||||
closing_debit, closing_credit = toggle_debit_credit(opening_debit + debit, opening_credit + credit)
|
||||
closing_debit, closing_credit = flt(closing_debit, precision), flt(closing_credit, precision)
|
||||
row.update({"closing_debit": closing_debit, "closing_credit": closing_credit})
|
||||
|
||||
row.update({"currency": company_currency})
|
||||
|
||||
@@ -304,9 +304,6 @@ def get_payment_entries(filters, args):
|
||||
pe.mode_of_payment,
|
||||
pe.project,
|
||||
pe.cost_center,
|
||||
pe.payment_type,
|
||||
pe.source_exchange_rate,
|
||||
pe.target_exchange_rate,
|
||||
)
|
||||
.where(
|
||||
(pe.docstatus == 1)
|
||||
@@ -317,22 +314,6 @@ def get_payment_entries(filters, args):
|
||||
)
|
||||
query = apply_common_conditions(filters, query, doctype="Payment Entry", payments=True)
|
||||
payment_entries = query.run(as_dict=True)
|
||||
|
||||
if payment_entries:
|
||||
ded = frappe.qb.DocType("Payment Entry Deduction")
|
||||
deduction_totals = frappe._dict(
|
||||
frappe.qb.from_(ded)
|
||||
.select(ded.parent, Sum(ded.amount))
|
||||
.where(ded.parent.isin([d.name for d in payment_entries]) & (ded.is_exchange_gain_loss == 0))
|
||||
.groupby(ded.parent)
|
||||
.run()
|
||||
)
|
||||
for d in payment_entries:
|
||||
exchange_rate = (
|
||||
d.source_exchange_rate if d.payment_type == "Receive" else d.target_exchange_rate
|
||||
) or 1
|
||||
d.base_grand_total = flt(d.base_grand_total) + flt(deduction_totals.get(d.name)) / exchange_rate
|
||||
|
||||
return payment_entries
|
||||
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||
from erpnext.accounts.party import get_party_account, get_party_account_currency
|
||||
from erpnext.buying.utils import validate_for_items
|
||||
from erpnext.controllers.buying_controller import BuyingController
|
||||
from erpnext.controllers.mapper import get_qty_already_mapped
|
||||
from erpnext.manufacturing.doctype.blanket_order.blanket_order import (
|
||||
validate_against_blanket_order,
|
||||
)
|
||||
@@ -770,16 +769,13 @@ def make_purchase_receipt(source_name, target_doc=None, args=None):
|
||||
def is_unit_price_row(source):
|
||||
return has_unit_price_items and source.qty == 0
|
||||
|
||||
mapped_qty_by_item = get_qty_already_mapped(target_doc, "purchase_order_item")
|
||||
|
||||
def update_item(obj, target, source_parent):
|
||||
received_qty = flt(obj.received_qty) + flt(mapped_qty_by_item.get(obj.name, 0))
|
||||
pending_qty = flt(obj.qty) - received_qty
|
||||
|
||||
target.qty = flt(obj.qty) if is_unit_price_row(obj) else pending_qty
|
||||
target.stock_qty = pending_qty * flt(obj.conversion_factor)
|
||||
target.amount = pending_qty * flt(obj.rate)
|
||||
target.base_amount = pending_qty * flt(obj.rate) * flt(source_parent.conversion_rate)
|
||||
target.qty = flt(obj.qty) if is_unit_price_row(obj) else flt(obj.qty) - flt(obj.received_qty)
|
||||
target.stock_qty = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.conversion_factor)
|
||||
target.amount = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate)
|
||||
target.base_amount = (
|
||||
(flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate) * flt(source_parent.conversion_rate)
|
||||
)
|
||||
|
||||
def select_item(d):
|
||||
filtered_items = args.get("filtered_children", [])
|
||||
@@ -811,9 +807,7 @@ def make_purchase_receipt(source_name, target_doc=None, args=None):
|
||||
},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: (
|
||||
doc.name not in mapped_qty_by_item
|
||||
if is_unit_price_row(doc)
|
||||
else abs(doc.received_qty) + abs(mapped_qty_by_item.get(doc.name, 0)) < abs(doc.qty)
|
||||
True if is_unit_price_row(doc) else abs(doc.received_qty) < abs(doc.qty)
|
||||
)
|
||||
and doc.delivered_by_supplier != 1
|
||||
and select_item(doc),
|
||||
@@ -872,13 +866,9 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
|
||||
)
|
||||
return query.run(pluck="qty")[0] or 0
|
||||
|
||||
mapped_qty_by_item = get_qty_already_mapped(target_doc, "po_detail")
|
||||
|
||||
def get_billed_and_mapped_qty(po_item_name):
|
||||
return flt(get_billed_qty(po_item_name)) + flt(mapped_qty_by_item.get(po_item_name, 0))
|
||||
|
||||
def update_item(obj, target, source_parent):
|
||||
target.qty = flt(obj.qty) - get_billed_and_mapped_qty(obj.name)
|
||||
billed_qty = flt(get_billed_qty(obj.name))
|
||||
target.qty = flt(obj.qty) - billed_qty
|
||||
|
||||
item = get_item_defaults(target.item_code, source_parent.company)
|
||||
item_group = get_item_group_defaults(target.item_code, source_parent.company)
|
||||
@@ -921,7 +911,6 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
|
||||
or abs(doc.billed_amt) < abs(doc.amount)
|
||||
or doc.qty > flt(get_billed_qty(doc.name))
|
||||
)
|
||||
and (doc.name not in mapped_qty_by_item or doc.qty > get_billed_and_mapped_qty(doc.name))
|
||||
and select_item(doc),
|
||||
},
|
||||
"Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges", "reset_value": True},
|
||||
|
||||
@@ -559,7 +559,6 @@
|
||||
"fieldname": "blanket_order_rate",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Blanket Order Rate",
|
||||
"options": "currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -955,7 +954,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-08-27 10:55:37.000000",
|
||||
"modified": "2026-08-07 17:31:31.732720",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Item",
|
||||
|
||||
@@ -10,7 +10,6 @@ from frappe.contacts.address_and_contact import (
|
||||
load_address_and_contact,
|
||||
)
|
||||
from frappe.model.naming import set_name_by_naming_series, set_name_from_naming_options
|
||||
from frappe.utils import get_link_to_form
|
||||
|
||||
from erpnext.accounts.party import (
|
||||
get_dashboard_info,
|
||||
@@ -182,15 +181,10 @@ class Supplier(TransactionBase):
|
||||
)
|
||||
|
||||
if internal_supplier:
|
||||
internal_supplier_link = get_link_to_form("Supplier", internal_supplier)
|
||||
frappe.throw(
|
||||
_(
|
||||
"Internal Supplier {0} already exists for {1}. Disable it to make this Supplier internal."
|
||||
).format(
|
||||
internal_supplier_link,
|
||||
frappe.bold(self.represents_company),
|
||||
),
|
||||
title=_("Internal Supplier Already Exists"),
|
||||
_("Internal Supplier for company {0} already exists").format(
|
||||
frappe.bold(self.represents_company)
|
||||
)
|
||||
)
|
||||
|
||||
def create_primary_contact(self):
|
||||
|
||||
@@ -11,7 +11,6 @@ from frappe.utils import flt, getdate, nowdate
|
||||
|
||||
from erpnext.buying.utils import validate_for_items
|
||||
from erpnext.controllers.buying_controller import BuyingController
|
||||
from erpnext.controllers.mapper import get_qty_already_mapped
|
||||
|
||||
form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
|
||||
|
||||
@@ -247,8 +246,6 @@ def make_purchase_order(source_name, target_doc=None, args=None):
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
|
||||
mapped_items = get_qty_already_mapped(target_doc, "supplier_quotation_item")
|
||||
|
||||
def set_missing_values(source, target):
|
||||
target.run_method("set_missing_values")
|
||||
target.run_method("get_schedule_dates")
|
||||
@@ -283,8 +280,7 @@ def make_purchase_order(source_name, target_doc=None, args=None):
|
||||
["sales_order", "sales_order"],
|
||||
],
|
||||
"postprocess": update_item,
|
||||
# no qty tracking between the two, so dedupe on the row reference alone
|
||||
"condition": lambda d: d.name not in mapped_items and select_item(d),
|
||||
"condition": select_item,
|
||||
},
|
||||
"Purchase Taxes and Charges": {
|
||||
"doctype": "Purchase Taxes and Charges",
|
||||
|
||||
@@ -10,26 +10,6 @@ frappe.query_reports["Purchase Analytics"] = {
|
||||
options: ["Supplier Group", "Supplier", "Item Group", "Item"],
|
||||
default: "Supplier",
|
||||
reqd: 1,
|
||||
on_change: function () {
|
||||
const entity_filter = frappe.query_report.get_filter("entity");
|
||||
if (entity_filter) {
|
||||
entity_filter.df.label = __(frappe.query_report.get_filter_value("tree_type"));
|
||||
entity_filter.set_value([]);
|
||||
entity_filter.refresh();
|
||||
}
|
||||
frappe.query_report.refresh();
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldname: "entity",
|
||||
label: __("Entity"),
|
||||
fieldtype: "MultiSelectList",
|
||||
get_data: function (txt) {
|
||||
const tree_type = frappe.query_report.get_filter_value("tree_type");
|
||||
if (!tree_type || tree_type === "Order Type") return [];
|
||||
return frappe.db.get_link_options(tree_type, txt);
|
||||
},
|
||||
depends_on: "eval:doc.tree_type != 'Order Type'",
|
||||
},
|
||||
{
|
||||
fieldname: "doc_type",
|
||||
@@ -85,19 +65,6 @@ frappe.query_reports["Purchase Analytics"] = {
|
||||
default: "Monthly",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "curves",
|
||||
label: __("Curves"),
|
||||
fieldtype: "Select",
|
||||
options: [
|
||||
{ value: "select", label: __("Select") },
|
||||
{ value: "all", label: __("All") },
|
||||
{ value: "non-zeros", label: __("Non-Zeros") },
|
||||
{ value: "total", label: __("Total Only") },
|
||||
],
|
||||
default: "select",
|
||||
reqd: 1,
|
||||
},
|
||||
{
|
||||
fieldname: "show_aggregate_value_from_subsidiary_companies",
|
||||
label: __("Show Aggregate Value from Subsidiary Companies"),
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.buying.report.purchase_analytics.purchase_analytics import execute
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
COMPANY = "_Test Company"
|
||||
SUPPLIER = "_Test Supplier"
|
||||
SUPPLIER_GROUP = "_Test Supplier Group"
|
||||
# A historical window that ordinary test fixtures don't post into.
|
||||
FROM_DATE = "2019-04-01"
|
||||
TO_DATE = "2019-06-30"
|
||||
|
||||
|
||||
class TestPurchaseAnalytics(ERPNextTestSuite):
|
||||
"""purchase_analytics reuses the shared Analytics engine; these tests lock its
|
||||
wiring (doc_type=Purchase Order) across the Supplier Group / Item Group trees."""
|
||||
|
||||
def setUp(self):
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
def _filters(self, **overrides):
|
||||
filters = {
|
||||
"doc_type": "Purchase Order",
|
||||
"value_quantity": "Value",
|
||||
"range": "Monthly",
|
||||
"company": COMPANY,
|
||||
"from_date": FROM_DATE,
|
||||
"to_date": TO_DATE,
|
||||
}
|
||||
filters.update(overrides)
|
||||
return frappe._dict(filters)
|
||||
|
||||
def _rows(self, filters):
|
||||
return {row["entity"]: row for row in execute(filters)[1]}
|
||||
|
||||
def make_po(self, qty=4, rate=250):
|
||||
return create_purchase_order(
|
||||
company=COMPANY, supplier=SUPPLIER, qty=qty, rate=rate, transaction_date="2019-04-10"
|
||||
)
|
||||
|
||||
def test_supplier_entity_filter(self):
|
||||
filters = self._filters(tree_type="Supplier", entity=[SUPPLIER], curves="all")
|
||||
base_total = flt(self._rows(filters).get(SUPPLIER, {}).get("total", 0.0))
|
||||
|
||||
po = self.make_po()
|
||||
columns, data, _message, chart, *_rest = execute(filters)
|
||||
|
||||
self.assertTrue(columns)
|
||||
self.assertEqual({row["entity"] for row in data}, {SUPPLIER})
|
||||
self.assertAlmostEqual(data[0]["total"] - base_total, flt(po.base_net_total), places=2)
|
||||
|
||||
supplier_name = frappe.db.get_value("Supplier", SUPPLIER, "supplier_name")
|
||||
self.assertEqual({dataset["name"] for dataset in chart["data"]["datasets"]}, {supplier_name})
|
||||
|
||||
def test_parent_supplier_group_filter_preserves_rollup(self):
|
||||
self.make_po()
|
||||
filters = self._filters(tree_type="Supplier Group")
|
||||
unfiltered = self._rows(filters)
|
||||
filtered = self._rows(self._filters(tree_type="Supplier Group", entity=["All Supplier Groups"]))
|
||||
|
||||
self.assertEqual(set(filtered), {"All Supplier Groups"})
|
||||
self.assertAlmostEqual(
|
||||
filtered["All Supplier Groups"]["total"],
|
||||
unfiltered["All Supplier Groups"]["total"],
|
||||
places=2,
|
||||
)
|
||||
|
||||
def test_supplier_group_entity_filter(self):
|
||||
self.make_po()
|
||||
unfiltered = self._rows(self._filters(tree_type="Supplier Group"))
|
||||
filtered = self._rows(self._filters(tree_type="Supplier Group", entity=[SUPPLIER_GROUP]))
|
||||
|
||||
self.assertEqual(set(filtered), {SUPPLIER_GROUP})
|
||||
self.assertEqual(filtered[SUPPLIER_GROUP]["indent"], 0)
|
||||
self.assertAlmostEqual(
|
||||
filtered[SUPPLIER_GROUP]["total"], unfiltered[SUPPLIER_GROUP]["total"], places=2
|
||||
)
|
||||
|
||||
def test_supplier_group_tree_rolls_up_to_root(self):
|
||||
filters = self._filters(tree_type="Supplier Group")
|
||||
base = self._rows(filters)
|
||||
base_group = flt(base.get(SUPPLIER_GROUP, {}).get("total", 0.0))
|
||||
|
||||
po = self.make_po(qty=4, rate=250)
|
||||
rows = self._rows(filters)
|
||||
|
||||
# supplier is remapped to its group; the root sits at indent 0
|
||||
self.assertIn(SUPPLIER_GROUP, rows)
|
||||
self.assertIn("All Supplier Groups", rows)
|
||||
self.assertNotIn(SUPPLIER, rows)
|
||||
self.assertEqual(rows["All Supplier Groups"]["indent"], 0)
|
||||
|
||||
self.assertAlmostEqual(rows[SUPPLIER_GROUP]["total"] - base_group, flt(po.base_net_total), places=2)
|
||||
self.assertGreaterEqual(flt(rows["All Supplier Groups"]["total"]), flt(po.base_net_total))
|
||||
|
||||
def test_item_group_tree_rolls_up_to_root(self):
|
||||
item_group = frappe.db.get_value("Item", "_Test Item", "item_group")
|
||||
filters = self._filters(tree_type="Item Group")
|
||||
base = self._rows(filters)
|
||||
base_group = flt(base.get(item_group, {}).get("total", 0.0))
|
||||
|
||||
po = self.make_po(qty=4, rate=250)
|
||||
rows = self._rows(filters)
|
||||
|
||||
self.assertIn(item_group, rows)
|
||||
self.assertIn("All Item Groups", rows)
|
||||
# the raw item code must not leak as its own entity; the root sits at indent 0
|
||||
self.assertNotIn("_Test Item", rows)
|
||||
self.assertEqual(rows["All Item Groups"]["indent"], 0)
|
||||
self.assertAlmostEqual(rows[item_group]["total"] - base_group, flt(po.base_net_total), places=2)
|
||||
self.assertGreaterEqual(flt(rows["All Item Groups"]["total"]), flt(po.base_net_total))
|
||||
|
||||
def test_supplier_group_by_quantity(self):
|
||||
filters = self._filters(tree_type="Supplier Group", value_quantity="Quantity")
|
||||
base = self._rows(filters)
|
||||
base_qty = flt(base.get(SUPPLIER_GROUP, {}).get("total", 0.0))
|
||||
base_root_qty = flt(base.get("All Supplier Groups", {}).get("total", 0.0))
|
||||
|
||||
po = self.make_po(qty=7, rate=100)
|
||||
rows = self._rows(filters)
|
||||
|
||||
self.assertAlmostEqual(rows[SUPPLIER_GROUP]["total"] - base_qty, flt(po.total_qty), places=2)
|
||||
# the quantity must roll up to the root too, not just the leaf group
|
||||
self.assertAlmostEqual(
|
||||
rows["All Supplier Groups"]["total"] - base_root_qty, flt(po.total_qty), places=2
|
||||
)
|
||||
@@ -1305,11 +1305,6 @@ class AccountsController(TransactionBase):
|
||||
if self.get("taxes") or self.get("is_pos"):
|
||||
return
|
||||
|
||||
# set by the Opening Invoice Creation Tool, where the outstanding amount
|
||||
# entered against a party is already inclusive of tax
|
||||
if self.flags.dont_auto_add_taxes:
|
||||
return
|
||||
|
||||
if frappe.get_single_value(
|
||||
"Accounts Settings", "add_taxes_from_taxes_and_charges_template"
|
||||
) and hasattr(self, "taxes_and_charges"):
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import flt
|
||||
|
||||
|
||||
def get_qty_already_mapped(target_doc, ref_field: str, qty_field: str = "qty") -> frappe._dict:
|
||||
"""Return a map: {source row name: qty} of rows already mapped into the target document.
|
||||
|
||||
"Get Items From" passes the in-progress (unsaved) document back as `target_doc`. Its rows
|
||||
are invisible to the pending-qty queries in the mappers, which only count submitted
|
||||
documents -- so without this, selecting the same source document twice maps every row
|
||||
again. Rows are keyed by `ref_field` (dn_detail, so_detail, ...), and a row is present in
|
||||
the map even when its qty is 0, so mappers without qty tracking can dedupe on presence.
|
||||
"""
|
||||
if isinstance(target_doc, str):
|
||||
target_doc = frappe.parse_json(target_doc)
|
||||
|
||||
qty_map = frappe._dict()
|
||||
for row in (target_doc and target_doc.get("items")) or []:
|
||||
if ref := row.get(ref_field):
|
||||
qty_map[ref] = qty_map.get(ref, 0) + flt(row.get(qty_field))
|
||||
|
||||
return qty_map
|
||||
@@ -639,7 +639,7 @@ def get_blanket_orders(doctype, txt, searchfield, start, page_len, filters):
|
||||
bo = frappe.qb.DocType("Blanket Order")
|
||||
bo_item = frappe.qb.DocType("Blanket Order Item")
|
||||
|
||||
query = (
|
||||
blanket_orders = (
|
||||
frappe.qb.from_(bo)
|
||||
.from_(bo_item)
|
||||
.select(bo.name)
|
||||
@@ -652,12 +652,10 @@ def get_blanket_orders(doctype, txt, searchfield, start, page_len, filters):
|
||||
& (bo.company == filters.get("company"))
|
||||
& (bo.docstatus == 1)
|
||||
)
|
||||
.run()
|
||||
)
|
||||
|
||||
if currency := filters.get("currency"):
|
||||
query = query.where(bo.currency == currency)
|
||||
|
||||
return query.run()
|
||||
return blanket_orders
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -75,7 +75,7 @@ SECONDARY_ITEM_PURPOSES = ("Manufacture", "Repack", "Disassemble")
|
||||
|
||||
def is_inspection_exempt_secondary_row(doc, row) -> bool:
|
||||
"""Whether the row is a secondary item on a document that produces secondary items."""
|
||||
if not (row.get("secondary_item_type") or row.get("valuation_type")):
|
||||
if not (row.get("secondary_item_type") or row.get("is_legacy_scrap_item")):
|
||||
return False
|
||||
|
||||
if doc.doctype == "Stock Entry":
|
||||
@@ -86,7 +86,9 @@ def is_inspection_exempt_secondary_row(doc, row) -> bool:
|
||||
|
||||
def stock_entry_row_requires_inspection(purpose, row):
|
||||
"""Check if this Stock Entry row need a Quality Inspection."""
|
||||
if purpose in SECONDARY_ITEM_PURPOSES and (row.get("secondary_item_type") or row.get("valuation_type")):
|
||||
if purpose in SECONDARY_ITEM_PURPOSES and (
|
||||
row.get("secondary_item_type") or row.get("is_legacy_scrap_item")
|
||||
):
|
||||
return False
|
||||
if purpose == "Manufacture":
|
||||
return bool(row.is_finished_item)
|
||||
|
||||
@@ -161,7 +161,7 @@ class SubcontractingController(StockController):
|
||||
).format(item.idx, get_link_to_form("Item", item.item_code))
|
||||
)
|
||||
|
||||
if not item.get("secondary_item_type") and not item.get("valuation_type"):
|
||||
if not item.get("secondary_item_type") and not item.get("is_legacy_scrap_item"):
|
||||
if not is_sub_contracted_item:
|
||||
frappe.throw(
|
||||
_("Row {0}: Item {1} must be a subcontracted item.").format(item.idx, item.item_name)
|
||||
@@ -1288,10 +1288,10 @@ class SubcontractingController(StockController):
|
||||
total_amt = sum(
|
||||
flt(item.amount)
|
||||
for item in self.get("items")
|
||||
if not item.get("secondary_item_type") and not item.get("valuation_type")
|
||||
if not item.get("secondary_item_type") and not item.get("is_legacy_scrap_item")
|
||||
)
|
||||
for item in self.items:
|
||||
if not item.get("secondary_item_type") and not item.get("valuation_type"):
|
||||
if not item.get("secondary_item_type") and not item.get("is_legacy_scrap_item"):
|
||||
item.additional_cost_per_qty = (
|
||||
(item.amount * self.total_additional_costs) / total_amt
|
||||
) / item.qty
|
||||
@@ -1299,15 +1299,15 @@ class SubcontractingController(StockController):
|
||||
total_qty = sum(
|
||||
flt(item.qty)
|
||||
for item in self.get("items")
|
||||
if not item.get("secondary_item_type") and not item.get("valuation_type")
|
||||
if not item.get("secondary_item_type") and not item.get("is_legacy_scrap_item")
|
||||
)
|
||||
additional_cost_per_qty = self.total_additional_costs / total_qty
|
||||
for item in self.items:
|
||||
if not item.get("secondary_item_type") and not item.get("valuation_type"):
|
||||
if not item.get("secondary_item_type") and not item.get("is_legacy_scrap_item"):
|
||||
item.additional_cost_per_qty = additional_cost_per_qty
|
||||
else:
|
||||
for item in self.items:
|
||||
if not item.get("secondary_item_type") and not item.get("valuation_type"):
|
||||
if not item.get("secondary_item_type") and not item.get("is_legacy_scrap_item"):
|
||||
item.additional_cost_per_qty = 0
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -242,7 +242,7 @@ class SubcontractingInwardController:
|
||||
for item in self.get("items")
|
||||
if not item.is_finished_item
|
||||
and not item.secondary_item_type
|
||||
and not item.valuation_type
|
||||
and not item.is_legacy_scrap_item
|
||||
and frappe.get_cached_value("Item", item.item_code, "is_customer_provided_item")
|
||||
]
|
||||
|
||||
@@ -372,7 +372,7 @@ class SubcontractingInwardController:
|
||||
if self.purpose in ["Subcontracting Delivery", "Subcontracting Return", "Manufacture"]:
|
||||
for item in self.items:
|
||||
if (
|
||||
item.is_finished_item or item.secondary_item_type or item.valuation_type
|
||||
item.is_finished_item or item.secondary_item_type or item.is_legacy_scrap_item
|
||||
) and item.valuation_rate == 0:
|
||||
item.allow_zero_valuation_rate = 1
|
||||
|
||||
@@ -472,7 +472,7 @@ class SubcontractingInwardController:
|
||||
self.validate_delivery_on_save()
|
||||
else:
|
||||
for item in self.items:
|
||||
if not item.secondary_item_type and not item.valuation_type:
|
||||
if not item.secondary_item_type and not item.is_legacy_scrap_item:
|
||||
delivered_qty, returned_qty = frappe.get_value(
|
||||
"Subcontracting Inward Order Item",
|
||||
item.scio_detail,
|
||||
@@ -543,7 +543,7 @@ class SubcontractingInwardController:
|
||||
bold(
|
||||
frappe.get_cached_value(
|
||||
"Subcontracting Inward Order Item"
|
||||
if not item.secondary_item_type and not item.valuation_type
|
||||
if not item.secondary_item_type and not item.is_legacy_scrap_item
|
||||
else "Subcontracting Inward Order Secondary Item",
|
||||
item.scio_detail,
|
||||
"stock_uom",
|
||||
@@ -595,7 +595,7 @@ class SubcontractingInwardController:
|
||||
)
|
||||
|
||||
for item in [item for item in self.items if not item.is_finished_item]:
|
||||
if item.secondary_item_type or item.valuation_type:
|
||||
if item.secondary_item_type or item.is_legacy_scrap_item:
|
||||
scio_secondary_item = frappe.get_value(
|
||||
"Subcontracting Inward Order Secondary Item",
|
||||
{
|
||||
@@ -655,7 +655,7 @@ class SubcontractingInwardController:
|
||||
for item in self.items:
|
||||
doctype = (
|
||||
"Subcontracting Inward Order Item"
|
||||
if not item.secondary_item_type and not item.valuation_type
|
||||
if not item.secondary_item_type and not item.is_legacy_scrap_item
|
||||
else "Subcontracting Inward Order Secondary Item"
|
||||
)
|
||||
qty_map[doctype][item.scio_detail] += (
|
||||
@@ -791,7 +791,7 @@ class SubcontractingInwardController:
|
||||
items = [
|
||||
item
|
||||
for item in self.items
|
||||
if not item.is_finished_item and not item.secondary_item_type and not item.valuation_type
|
||||
if not item.is_finished_item and not item.secondary_item_type and not item.is_legacy_scrap_item
|
||||
]
|
||||
item_code_wh = frappe._dict(
|
||||
{
|
||||
@@ -894,7 +894,7 @@ class SubcontractingInwardController:
|
||||
def update_inward_order_secondary_items(self):
|
||||
if (scio := self.subcontracting_inward_order) and self.purpose == "Manufacture":
|
||||
secondary_items_list = [
|
||||
item for item in self.items if item.secondary_item_type or item.valuation_type
|
||||
item for item in self.items if item.secondary_item_type or item.is_legacy_scrap_item
|
||||
]
|
||||
|
||||
secondary_items = defaultdict(float)
|
||||
|
||||
@@ -31,97 +31,6 @@ class TestMapper(ERPNextTestSuite):
|
||||
src_items = item_list_1 + item_list_2 + item_list_3
|
||||
self.assertEqual(set(d for d in src_items), set(d.item_code for d in updated_so.items))
|
||||
|
||||
def test_get_items_from_is_idempotent(self):
|
||||
"""Selecting the same source document twice must not duplicate rows in the target.
|
||||
|
||||
"Get Items From" hands the in-progress document back to the mapper as `target_doc`.
|
||||
Its rows are unsaved, so the mappers' pending-qty queries (submitted documents only)
|
||||
cannot see them -- every mapper has to discount them explicitly.
|
||||
"""
|
||||
for label, make_source, method in self.idempotency_cases():
|
||||
with self.subTest(label):
|
||||
source = make_source()
|
||||
target = frappe.get_attr(method)(source.name)
|
||||
mapped_rows = len(target.items)
|
||||
self.assertTrue(mapped_rows, f"{label}: nothing was mapped")
|
||||
|
||||
target = frappe.get_attr(method)(source.name, target)
|
||||
self.assertEqual(len(target.items), mapped_rows, f"{label}: rows were duplicated")
|
||||
|
||||
def idempotency_cases(self):
|
||||
"""(label, source factory, mapper method) for every "Get Items From" button.
|
||||
|
||||
Quotation -> Sales Invoice is absent: Sales Invoice Item keeps no reference to the
|
||||
Quotation row, so there is nothing to deduplicate on.
|
||||
"""
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.selling.doctype.quotation.test_quotation import make_quotation
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
from erpnext.stock.doctype.material_request.test_material_request import (
|
||||
make_material_request_for_items,
|
||||
)
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
|
||||
self.load_test_records("Supplier Quotation")
|
||||
|
||||
def make_supplier_quotation():
|
||||
return frappe.copy_doc(self.globalTestRecords["Supplier Quotation"][0]).submit()
|
||||
|
||||
return [
|
||||
(
|
||||
"Quotation -> Sales Order",
|
||||
lambda: make_quotation(),
|
||||
"erpnext.selling.doctype.quotation.quotation.make_sales_order",
|
||||
),
|
||||
(
|
||||
"Sales Order -> Sales Invoice",
|
||||
lambda: make_sales_order(),
|
||||
"erpnext.selling.doctype.sales_order.sales_order.make_sales_invoice",
|
||||
),
|
||||
(
|
||||
"Sales Order -> Delivery Note",
|
||||
lambda: make_sales_order(),
|
||||
"erpnext.selling.doctype.sales_order.sales_order.make_delivery_note",
|
||||
),
|
||||
(
|
||||
"Delivery Note -> Sales Invoice",
|
||||
lambda: create_delivery_note(),
|
||||
"erpnext.stock.doctype.delivery_note.delivery_note.make_sales_invoice",
|
||||
),
|
||||
(
|
||||
"Material Request -> Purchase Order",
|
||||
lambda: make_material_request_for_items(["_Test Item"]),
|
||||
"erpnext.stock.doctype.material_request.material_request.make_purchase_order",
|
||||
),
|
||||
(
|
||||
"Supplier Quotation -> Purchase Order",
|
||||
make_supplier_quotation,
|
||||
"erpnext.buying.doctype.supplier_quotation.supplier_quotation.make_purchase_order",
|
||||
),
|
||||
(
|
||||
"Purchase Order -> Purchase Receipt",
|
||||
lambda: create_purchase_order(),
|
||||
"erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_receipt",
|
||||
),
|
||||
(
|
||||
"Purchase Order -> Purchase Invoice",
|
||||
lambda: create_purchase_order(),
|
||||
"erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_invoice",
|
||||
),
|
||||
(
|
||||
"Purchase Receipt -> Purchase Invoice",
|
||||
lambda: make_purchase_receipt(),
|
||||
"erpnext.stock.doctype.purchase_receipt.purchase_receipt.make_purchase_invoice",
|
||||
),
|
||||
(
|
||||
"Purchase Invoice -> Purchase Receipt",
|
||||
lambda: make_purchase_invoice(),
|
||||
"erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_purchase_receipt",
|
||||
),
|
||||
]
|
||||
|
||||
def make_quotation(self, item_list, customer):
|
||||
qtn = frappe.get_doc(
|
||||
{
|
||||
|
||||
@@ -39,7 +39,6 @@ def get_contract_template(template_name, doc):
|
||||
doc = json.loads(doc)
|
||||
|
||||
contract_template = frappe.get_doc("Contract Template", template_name)
|
||||
contract_template.check_permission()
|
||||
contract_terms = None
|
||||
|
||||
if contract_template.contract_terms:
|
||||
|
||||
@@ -29,18 +29,11 @@ class EmailCampaign(Document):
|
||||
|
||||
def validate(self):
|
||||
self.set_date()
|
||||
self.validate_recipient_email()
|
||||
self.validate_email_campaign_already_exists()
|
||||
self.update_status()
|
||||
|
||||
def validate_recipient_email(self):
|
||||
if not self.recipient:
|
||||
return
|
||||
|
||||
# checking if email is set for lead. Not checking for contact as email is a mandatory field for contact.
|
||||
if self.email_campaign_for == "Lead":
|
||||
self.validate_lead()
|
||||
elif self.email_campaign_for == "Contact":
|
||||
self.validate_contact()
|
||||
self.validate_email_campaign_already_exists()
|
||||
self.update_status()
|
||||
|
||||
def set_date(self):
|
||||
if getdate(self.start_date) < getdate(today()):
|
||||
@@ -63,13 +56,6 @@ class EmailCampaign(Document):
|
||||
lead_name = frappe.db.get_value("Lead", self.recipient, "lead_name")
|
||||
frappe.throw(_("Please set an email id for the Lead {0}").format(lead_name))
|
||||
|
||||
def validate_contact(self):
|
||||
contact = frappe.db.get_value("Contact", self.recipient, ["email_id", "full_name"], as_dict=True)
|
||||
if contact and not contact.email_id:
|
||||
frappe.throw(
|
||||
_("Please set a primary email ID for the Contact {0}").format(frappe.bold(contact.full_name))
|
||||
)
|
||||
|
||||
def validate_email_campaign_already_exists(self):
|
||||
email_campaign_exists = frappe.db.exists(
|
||||
"Email Campaign",
|
||||
|
||||
@@ -1,84 +1,10 @@
|
||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, getdate, today
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
|
||||
class TestEmailCampaign(ERPNextTestSuite):
|
||||
"""Email Campaign derives its window from the linked Campaign schedule and
|
||||
guards the start date and the recipient's email."""
|
||||
|
||||
def setUp(self):
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
def make_email_template(self):
|
||||
name = "_Test EC Email Template"
|
||||
if not frappe.db.exists("Email Template", name):
|
||||
frappe.get_doc(
|
||||
{"doctype": "Email Template", "name": name, "subject": "Test", "response": "Hello"}
|
||||
).insert()
|
||||
return name
|
||||
|
||||
def make_campaign(self, schedules):
|
||||
campaign = frappe.new_doc("Campaign")
|
||||
campaign.campaign_name = f"_Test EC Campaign {frappe.generate_hash(length=6)}"
|
||||
for days in schedules:
|
||||
campaign.append(
|
||||
"campaign_schedules",
|
||||
{"send_after_days": days, "email_template": self.make_email_template()},
|
||||
)
|
||||
return campaign.insert()
|
||||
|
||||
def make_email_campaign(self, campaign_name, start_date=None):
|
||||
doc = frappe.new_doc("Email Campaign")
|
||||
doc.campaign_name = campaign_name
|
||||
doc.start_date = start_date or today()
|
||||
return doc
|
||||
|
||||
def test_start_date_cannot_be_in_the_past(self):
|
||||
doc = self.make_email_campaign("irrelevant", start_date=add_days(today(), -1))
|
||||
self.assertRaises(frappe.ValidationError, doc.set_date)
|
||||
|
||||
def test_end_date_is_start_plus_max_send_after_days(self):
|
||||
campaign = self.make_campaign(schedules=[0, 5])
|
||||
doc = self.make_email_campaign(campaign.name)
|
||||
doc.set_date()
|
||||
self.assertEqual(getdate(doc.end_date), add_days(getdate(today()), 5))
|
||||
|
||||
def test_campaign_without_a_schedule_is_rejected(self):
|
||||
campaign = self.make_campaign(schedules=[])
|
||||
doc = self.make_email_campaign(campaign.name)
|
||||
self.assertRaises(frappe.ValidationError, doc.set_date)
|
||||
|
||||
def test_lead_without_an_email_is_rejected(self):
|
||||
lead = frappe.get_doc({"doctype": "Lead", "lead_name": "_Test Lead No Email"}).insert()
|
||||
doc = frappe.new_doc("Email Campaign")
|
||||
doc.email_campaign_for = "Lead"
|
||||
doc.recipient = lead.name
|
||||
self.assertRaises(frappe.ValidationError, doc.validate_lead)
|
||||
|
||||
def test_contact_without_an_email_is_rejected(self):
|
||||
contact = frappe.get_doc({"doctype": "Contact", "first_name": "_Test Contact No Email"}).insert()
|
||||
campaign = self.make_campaign(schedules=[0])
|
||||
doc = self.make_email_campaign(campaign.name)
|
||||
doc.email_campaign_for = "Contact"
|
||||
doc.recipient = contact.name
|
||||
self.assertRaisesRegex(frappe.ValidationError, "primary email ID", doc.insert)
|
||||
|
||||
def test_contact_with_an_email_is_accepted(self):
|
||||
contact = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Contact",
|
||||
"first_name": "_Test Contact With Email",
|
||||
"email_ids": [{"email_id": "_test_email_campaign@example.com", "is_primary": 1}],
|
||||
}
|
||||
).insert()
|
||||
campaign = self.make_campaign(schedules=[0])
|
||||
doc = self.make_email_campaign(campaign.name)
|
||||
doc.email_campaign_for = "Contact"
|
||||
doc.recipient = contact.name
|
||||
doc.insert()
|
||||
self.assertEqual(doc.status, "In Progress")
|
||||
pass
|
||||
|
||||
@@ -255,10 +255,7 @@ class CRMNote(Document):
|
||||
notify_mentions(self.doctype, self.name, note)
|
||||
|
||||
@frappe.whitelist()
|
||||
def edit_note(self, note: str, row_id: str):
|
||||
# db_update() skips the write check that save() does in add_note/delete_note
|
||||
self.check_permission("write")
|
||||
|
||||
def edit_note(self, note, row_id):
|
||||
for d in self.notes:
|
||||
if cstr(d.name) == row_id:
|
||||
d.note = note
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-08-09 09:47+0000\n"
|
||||
"PO-Revision-Date: 2026-08-26 11:43\n"
|
||||
"PO-Revision-Date: 2026-08-09 11:01\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Arabic\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-08-09 09:47+0000\n"
|
||||
"PO-Revision-Date: 2026-08-26 11:43\n"
|
||||
"PO-Revision-Date: 2026-08-09 11:01\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Bulgarian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-08-09 09:47+0000\n"
|
||||
"PO-Revision-Date: 2026-08-26 11:43\n"
|
||||
"PO-Revision-Date: 2026-08-10 11:05\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Bosnian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -353,7 +353,7 @@ msgstr "'Trajanje Važenja Verifikacijskog Linka' mora biti između 15 i 60 minu
|
||||
|
||||
#: erpnext/accounts/doctype/bank_account/bank_account.py:79
|
||||
msgid "'{0}' account is already used by {1}. Use another account."
|
||||
msgstr "Račun '{0}' već koristi {1}. Koristi drugi račun."
|
||||
msgstr "Račun '{0}' već koristi {1}. Koristite drugi račun."
|
||||
|
||||
#: erpnext/accounts/doctype/pos_settings/pos_settings.py:44
|
||||
msgid "'{0}' has been already added."
|
||||
@@ -9803,7 +9803,7 @@ msgstr "Nije moguće promijeniti atribute nakon transakcije zaliha. Napravi novi
|
||||
|
||||
#: erpnext/stock/doctype/item/item.py:1129
|
||||
msgid "Cannot change Item {0} from serialized to non-serialized because a Serial and Batch Bundle exists for it. Please delete or cancel the Serial and Batch Bundle first."
|
||||
msgstr "Nije moguće promijenuti artikal {0} iz serijaliziranog u neserijalizirani jer za njega postoji Serijski i Šaržni paket. Prvo obriši ili otkažite Serijski i Šaržni paket."
|
||||
msgstr "Nije moguće promijenuti artikal {0} iz serijaliziranog u neserijalizirani jer za njega postoji Serijski i Šaržni paket. Prvo izbrišite ili otkažite Serijski i Šaržni paket."
|
||||
|
||||
#: erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py:74
|
||||
msgid "Cannot change Reference Document Type."
|
||||
@@ -16413,29 +16413,29 @@ msgstr "Sažetak Odgođenih Zadataka"
|
||||
#. Settings'
|
||||
#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json
|
||||
msgid "Delete Accounting and Stock Ledger entries on deletion of transaction"
|
||||
msgstr "Obriši unose Knjigovodstva i Registra Zaliha pri brisanju Transakcije"
|
||||
msgstr "Izbriši unose Knjigovodstva i Registra Zaliha pri brisanju Transakcije"
|
||||
|
||||
#. Label of the delete_bin_data_status (Select) field in DocType 'Transaction
|
||||
#. Deletion Record'
|
||||
#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json
|
||||
msgid "Delete Bins"
|
||||
msgstr "Obriši Spremnike"
|
||||
msgstr "Izbriši Spremnike"
|
||||
|
||||
#. Label of the delete_cancelled_entries (Check) field in DocType 'Repost
|
||||
#. Accounting Ledger'
|
||||
#: erpnext/accounts/doctype/repost_accounting_ledger/repost_accounting_ledger.json
|
||||
msgid "Delete Cancelled Ledger Entries"
|
||||
msgstr "Obriši poništene unose iz Registra"
|
||||
msgstr "Izbrišite poništene unose iz Registra"
|
||||
|
||||
#. Label of a standard navbar item
|
||||
#. Type: Action
|
||||
#: erpnext/hooks.py erpnext/public/js/utils/demo.js:5
|
||||
msgid "Delete Demo Data"
|
||||
msgstr "Obriši Demo Podatke"
|
||||
msgstr "Izbriši Demo Podatke"
|
||||
|
||||
#: erpnext/stock/doctype/inventory_dimension/inventory_dimension.js:65
|
||||
msgid "Delete Dimension"
|
||||
msgstr "Obriši Dimenziju"
|
||||
msgstr "Izbriši Dimenziju"
|
||||
|
||||
#. Label of the delete_leads_and_addresses_status (Select) field in DocType
|
||||
#. 'Transaction Deletion Record'
|
||||
@@ -16447,14 +16447,14 @@ msgstr "Izriši Potencijalne Klijente i Adrese"
|
||||
#. in DocType 'Appointment Booking Settings'
|
||||
#: erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json
|
||||
msgid "Delete Permanently"
|
||||
msgstr "Trajno Obriši"
|
||||
msgstr "Trajno Izbriši"
|
||||
|
||||
#. Label of the delete_transactions_status (Select) field in DocType
|
||||
#. 'Transaction Deletion Record'
|
||||
#: erpnext/setup/doctype/company/company.js:179
|
||||
#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.json
|
||||
msgid "Delete Transactions"
|
||||
msgstr "Obriši Transakcije"
|
||||
msgstr "Izbriši Transakcije"
|
||||
|
||||
#: erpnext/setup/doctype/company/company.js:249
|
||||
msgid "Delete all the Transactions for {0}"
|
||||
@@ -18347,7 +18347,7 @@ msgstr "Kopiraj Grupa Klijenta"
|
||||
|
||||
#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:190
|
||||
msgid "Duplicate DocType"
|
||||
msgstr "Dupliraj DocType"
|
||||
msgstr "Dupliciraj DocType"
|
||||
|
||||
#: erpnext/setup/doctype/authorization_rule/authorization_rule.py:71
|
||||
msgid "Duplicate Entry. Please check Authorization Rule {0}"
|
||||
@@ -18363,7 +18363,7 @@ msgstr "Kopiraj Grupu Artikla"
|
||||
|
||||
#: erpnext/manufacturing/doctype/bom_creator/bom_creator.py:102
|
||||
msgid "Duplicate Item Under Same Parent"
|
||||
msgstr "Dupliraj Artikal pod Istim Nadređenim"
|
||||
msgstr "Dupliciraj Artikal pod Istim Nadređenim"
|
||||
|
||||
#: erpnext/manufacturing/doctype/workstation/workstation.py:80
|
||||
#: erpnext/manufacturing/doctype/workstation_type/workstation_type.py:37
|
||||
@@ -18372,7 +18372,7 @@ msgstr "Duplikat operativne komponente {0} je pronađen u operativnim komponenta
|
||||
|
||||
#: erpnext/accounts/doctype/pos_settings/pos_settings.py:44
|
||||
msgid "Duplicate POS Fields"
|
||||
msgstr "Dupliraj Kasa Polja"
|
||||
msgstr "Dupliciraj Kasa Polja"
|
||||
|
||||
#: erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py:104
|
||||
#: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py:64
|
||||
@@ -18409,7 +18409,7 @@ msgstr "Dupli unos naspram koda artikla {0} i proizvođača {1}"
|
||||
|
||||
#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:189
|
||||
msgid "Duplicate entry: {0}{1}"
|
||||
msgstr "Dupliraj unos: {0}{1}"
|
||||
msgstr "Dupliciraj unos: {0}{1}"
|
||||
|
||||
#: erpnext/accounts/doctype/pos_profile/pos_profile.py:164
|
||||
msgid "Duplicate item group found in the item group table"
|
||||
@@ -20376,7 +20376,7 @@ msgstr "Brisanje pravila nije uspjelo."
|
||||
|
||||
#: erpnext/setup/demo.py:77
|
||||
msgid "Failed to erase demo data, please delete the demo company manually."
|
||||
msgstr "Brisanje demo podataka nije uspjelo, obriši demo poduzeće ručno."
|
||||
msgstr "Brisanje demo podataka nije uspjelo, izbrišite demo poduzeće ručno."
|
||||
|
||||
#: erpnext/setup/setup_wizard/setup_wizard.py:17
|
||||
#: erpnext/setup/setup_wizard/setup_wizard.py:18
|
||||
@@ -21932,7 +21932,7 @@ msgstr "Zatvoreno"
|
||||
#. Description of the 'Is Frozen' (Check) field in DocType 'Supplier'
|
||||
#: erpnext/buying/doctype/supplier/supplier.json
|
||||
msgid "Frozen suppliers block ledger entries until unfrozen. Use this to temporarily lock accounting activity without disabling the supplier."
|
||||
msgstr "Zatvoreni dobavljači blokiraju unose u registar dok se ne otvore. Koristi ovo za privremeno zaključavanje knjigovodstvenih aktivnosti bez onemogućavanja dobavljača."
|
||||
msgstr "Zatvoreni dobavljači blokiraju unose u registar dok se ne otvore. Koristite ovo za privremeno zaključavanje knjigovodstvenih aktivnosti bez onemogućavanja dobavljača."
|
||||
|
||||
#. Label of the fuel_type (Select) field in DocType 'Vehicle'
|
||||
#: erpnext/setup/doctype/vehicle/vehicle.json
|
||||
@@ -38051,7 +38051,7 @@ msgstr "Izradi Nabavni Račun ili Nabavnu Fakturu za artikal {0}"
|
||||
|
||||
#: erpnext/stock/doctype/item/item.py:706
|
||||
msgid "Please delete Product Bundle {0}, before merging {1} into {2}"
|
||||
msgstr "Obriši Artikal Paket {0}, prije spajanja {1} u {2}"
|
||||
msgstr "Izbriši Artikal Paket {0}, prije spajanja {1} u {2}"
|
||||
|
||||
#: erpnext/assets/doctype/asset/depreciation.py:562
|
||||
msgid "Please disable workflow temporarily for Journal Entry {0}"
|
||||
@@ -41743,7 +41743,7 @@ msgstr "Pravilo Odlaganja već postoji za Artikal {0} u Skladištu {1}."
|
||||
#. DocType 'Inventory Dimension'
|
||||
#: erpnext/stock/doctype/inventory_dimension/inventory_dimension.json
|
||||
msgid "Python expression evaluated on the server. Use doc.fieldname for the row and parent.fieldname for the parent document. When it evaluates to true the dimension becomes mandatory. Example: doc.t_warehouse and doc.qty > 0"
|
||||
msgstr "Python izraz se računa na serveru. Koristi doc.fieldname za red i parent.fieldname za nadređeni dokument. Kada se računa kao istinito, dimenzija postaje obavezna. Primjer: doc.t_warehouse i doc.qty > 0"
|
||||
msgstr "Python izraz se računa na serveru. Koristite doc.fieldname za red i parent.fieldname za nadređeni dokument. Kada se računa kao istinito, dimenzija postaje obavezna. Primjer: doc.t_warehouse i doc.qty > 0"
|
||||
|
||||
#: banking/src/components/features/BankReconciliation/BankRecDateFilter.tsx:41
|
||||
msgid "Q1"
|
||||
@@ -45902,7 +45902,7 @@ msgstr "Uloga obavještavanja o neuspjehu amortizacije"
|
||||
#. 'Company'
|
||||
#: erpnext/setup/doctype/company/company.json
|
||||
msgid "Roles Allowed to Set and Edit Frozen Account Entries"
|
||||
msgstr "Uloge kojima je dozvoljeno postavljanje i uređivanje unosa zamrznutih računa"
|
||||
msgstr "Uloge kojima je dozvoljeno postavljanje i uređivanje unosa zatvorenih računa"
|
||||
|
||||
#. Label of the root (Link) field in DocType 'Bisect Nodes'
|
||||
#: erpnext/accounts/doctype/bisect_nodes/bisect_nodes.json
|
||||
@@ -46970,7 +46970,7 @@ msgstr "Red {0}: Dodijeljeni iznos {1} mora biti manji ili jednak preostalom izn
|
||||
|
||||
#: erpnext/stock/doctype/stock_entry/stock_entry.py:1663
|
||||
msgid "Row {0}: As {1} is enabled, raw materials cannot be added to {2} entry. Use {3} entry to consume raw materials."
|
||||
msgstr "Red {0}: Kako je {1} omogućen, sirovine se ne mogu dodati u {2} unos. Koristi {3} unos za potrošnju sirovina."
|
||||
msgstr "Red {0}: Kako je {1} omogućen, sirovine se ne mogu dodati u {2} unos. Koristite {3} unos za potrošnju sirovina."
|
||||
|
||||
#: erpnext/stock/doctype/material_request/material_request.py:1052
|
||||
msgid "Row {0}: Bill of Materials not found for the Item {1}"
|
||||
@@ -52263,7 +52263,7 @@ msgstr "Zaliha Primljena, ali nije Fakturisana"
|
||||
#: erpnext/stock/workspace/stock/stock.json
|
||||
#: erpnext/workspace_sidebar/stock.json
|
||||
msgid "Stock Reconciliation"
|
||||
msgstr "Lista Zaliha"
|
||||
msgstr "Popis Zaliha"
|
||||
|
||||
#. Name of a DocType
|
||||
#: erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
|
||||
@@ -59396,7 +59396,7 @@ msgstr "Hitno"
|
||||
|
||||
#: erpnext/accounts/doctype/repost_payment_ledger/repost_payment_ledger.js:36
|
||||
msgid "Use 'Repost in background' button to trigger background job. Job can only be triggered when document is in Queued or Failed status."
|
||||
msgstr "Koristi dugme 'Ponovo knjiži u pozadini' da pokrenete posao u pozadini. Zadatak se može pokrenuti samo kada je dokument u stanju čekanja ili neuspješan."
|
||||
msgstr "Koristite dugme 'Ponovo knjiži u pozadini' da pokrenete posao u pozadini. Zadatak se može pokrenuti samo kada je dokument u stanju čekanja ili neuspješan."
|
||||
|
||||
#. Description of the 'Advanced Filtering' (Check) field in DocType 'Financial
|
||||
#. Report Row'
|
||||
@@ -59472,7 +59472,7 @@ msgstr "Koristi Višeslojnu Sastavnicu"
|
||||
#. DocType 'Global Defaults'
|
||||
#: erpnext/setup/doctype/global_defaults/global_defaults.json
|
||||
msgid "Use Posting Datetime for Naming Documents"
|
||||
msgstr "Koristi datum i vrijeme registracije za Imenovanje Dokumenata"
|
||||
msgstr "Koristite datum i vrijeme registracije za Imenovanje Dokumenata"
|
||||
|
||||
#. Label of the use_serial_batch_fields (Check) field in DocType 'Stock
|
||||
#. Settings'
|
||||
@@ -59533,7 +59533,7 @@ msgstr "Koristi Devizni Kurs Datuma Transakcije"
|
||||
|
||||
#: erpnext/projects/doctype/project/project.py:600
|
||||
msgid "Use a name that is different from previous project name"
|
||||
msgstr "Koristi naziv koji se razlikuje od naziva prethodnog projekta"
|
||||
msgstr "Koristite naziv koji se razlikuje od naziva prethodnog projekta"
|
||||
|
||||
#. Label of the use_for_shopping_cart (Check) field in DocType 'Tax Rule'
|
||||
#: erpnext/accounts/doctype/tax_rule/tax_rule.json
|
||||
@@ -59550,13 +59550,13 @@ msgstr "Koristi stari Kontroler Proračuna"
|
||||
#. 'Accounts Settings'
|
||||
#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json
|
||||
msgid "Use legacy controller for Period Closing Voucher"
|
||||
msgstr "Koristi stari kontroler za Verifikat Zatvaranje Perioda"
|
||||
msgstr "Koristite stari kontroler za Verifikat Zatvaranje Perioda"
|
||||
|
||||
#. Label of the fallback_to_default_price_list (Check) field in DocType
|
||||
#. 'Selling Settings'
|
||||
#: erpnext/selling/doctype/selling_settings/selling_settings.json
|
||||
msgid "Use prices from Default Price List as fallback"
|
||||
msgstr "Koristi cjene iz Standard Cjenovnika kao Rezervnu Opciju"
|
||||
msgstr "Koristite cjene iz Standard Cjenovnika kao Rezervnu Opciju"
|
||||
|
||||
#. Description of the 'Sales Order Date' (Date) field in DocType 'Sales Order
|
||||
#. Item'
|
||||
@@ -62485,7 +62485,7 @@ msgstr "po satu"
|
||||
|
||||
#: erpnext/stock/stock_ledger.py:2092
|
||||
msgid "performing either one below:"
|
||||
msgstr "izvodi bilo koje niže:"
|
||||
msgstr "izvodi bilo koje dolje:"
|
||||
|
||||
#. Description of the 'Product Bundle Item' (Data) field in DocType 'Pick List
|
||||
#. Item'
|
||||
@@ -63005,7 +63005,7 @@ msgstr "{0} transakcija će biti uvezeno u sistem. Molimo Vas da pregledate deta
|
||||
|
||||
#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:732
|
||||
msgid "{0} units are reserved for Item {1} in Warehouse {2}, please un-reserve the same to {3} the Stock Reconciliation."
|
||||
msgstr "{0} jedinica je rezervisano za artikal {1} u Skladištu {2}, poništi rezervaciju iste za {3} Listu Zaliha."
|
||||
msgstr "{0} jedinica je rezervisano za artikal {1} u Skladištu {2}, poništi rezervaciju iste za {3} Popis Zaliha."
|
||||
|
||||
#: erpnext/stock/doctype/pick_list/pick_list.py:1120
|
||||
msgid "{0} units of Item {1} is not available in any of the warehouses."
|
||||
@@ -63297,7 +63297,7 @@ msgstr "{0}: ukloni nevažeću vrijednost(i) {1}"
|
||||
|
||||
#: erpnext/stock/doctype/item/item.js:897
|
||||
msgid "{0}: select the typed value {1} from the list or clear it"
|
||||
msgstr "{0}: odaberi unesenu vrijednost {1} s liste ili je obriši"
|
||||
msgstr "{0}: odaberi unesenu vrijednost {1} s liste ili je obrišite"
|
||||
|
||||
#: erpnext/controllers/accounts_controller.py:567
|
||||
msgid "{0}: {1} does not belong to the Company: {2}"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-08-09 09:47+0000\n"
|
||||
"PO-Revision-Date: 2026-08-26 11:43\n"
|
||||
"PO-Revision-Date: 2026-08-09 11:01\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Czech\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-08-09 09:47+0000\n"
|
||||
"PO-Revision-Date: 2026-08-26 11:43\n"
|
||||
"PO-Revision-Date: 2026-08-09 11:01\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Danish\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-08-09 09:47+0000\n"
|
||||
"PO-Revision-Date: 2026-08-26 11:43\n"
|
||||
"PO-Revision-Date: 2026-08-09 11:01\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: German\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-08-09 09:47+0000\n"
|
||||
"PO-Revision-Date: 2026-08-26 11:44\n"
|
||||
"PO-Revision-Date: 2026-08-09 11:03\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Esperanto\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-08-09 09:47+0000\n"
|
||||
"PO-Revision-Date: 2026-08-26 11:43\n"
|
||||
"PO-Revision-Date: 2026-08-09 11:01\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Spanish\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-08-09 09:47+0000\n"
|
||||
"PO-Revision-Date: 2026-08-26 11:43\n"
|
||||
"PO-Revision-Date: 2026-08-09 11:01\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: French\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-08-09 09:47+0000\n"
|
||||
"PO-Revision-Date: 2026-08-26 11:43\n"
|
||||
"PO-Revision-Date: 2026-08-09 11:03\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Hindi\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-08-09 09:47+0000\n"
|
||||
"PO-Revision-Date: 2026-08-26 11:43\n"
|
||||
"PO-Revision-Date: 2026-08-10 11:05\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Croatian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
18439
erpnext/locale/hu.po
18439
erpnext/locale/hu.po
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-08-09 09:47+0000\n"
|
||||
"PO-Revision-Date: 2026-08-26 11:43\n"
|
||||
"PO-Revision-Date: 2026-08-09 11:02\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Indonesian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-08-09 09:47+0000\n"
|
||||
"PO-Revision-Date: 2026-08-26 11:43\n"
|
||||
"PO-Revision-Date: 2026-08-09 11:01\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Italian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
63234
erpnext/locale/km.po
63234
erpnext/locale/km.po
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-08-09 09:47+0000\n"
|
||||
"PO-Revision-Date: 2026-08-26 11:43\n"
|
||||
"PO-Revision-Date: 2026-08-10 11:05\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Korean\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
63373
erpnext/locale/mn.po
63373
erpnext/locale/mn.po
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-08-09 09:47+0000\n"
|
||||
"PO-Revision-Date: 2026-08-26 11:43\n"
|
||||
"PO-Revision-Date: 2026-08-09 11:03\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Burmese\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-08-09 09:47+0000\n"
|
||||
"PO-Revision-Date: 2026-08-26 11:43\n"
|
||||
"PO-Revision-Date: 2026-08-09 11:03\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Norwegian Bokmal\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-08-09 09:47+0000\n"
|
||||
"PO-Revision-Date: 2026-08-26 11:43\n"
|
||||
"PO-Revision-Date: 2026-08-09 11:01\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Dutch\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-08-09 09:47+0000\n"
|
||||
"PO-Revision-Date: 2026-08-26 11:43\n"
|
||||
"PO-Revision-Date: 2026-08-09 11:01\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Polish\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-08-09 09:47+0000\n"
|
||||
"PO-Revision-Date: 2026-08-26 11:43\n"
|
||||
"PO-Revision-Date: 2026-08-09 11:02\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Portuguese\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-08-09 09:47+0000\n"
|
||||
"PO-Revision-Date: 2026-08-26 11:43\n"
|
||||
"PO-Revision-Date: 2026-08-09 11:02\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Portuguese, Brazilian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-08-09 09:47+0000\n"
|
||||
"PO-Revision-Date: 2026-08-26 11:43\n"
|
||||
"PO-Revision-Date: 2026-08-09 11:01\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Romanian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-08-09 09:47+0000\n"
|
||||
"PO-Revision-Date: 2026-08-26 11:43\n"
|
||||
"PO-Revision-Date: 2026-08-09 11:02\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Russian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -12892,13 +12892,13 @@ msgstr "Расходы"
|
||||
#. Label of the cost_allocation (Currency) field in DocType 'BOM'
|
||||
#: erpnext/manufacturing/doctype/bom/bom.json
|
||||
msgid "Cost Allocation"
|
||||
msgstr "Распределение расходов"
|
||||
msgstr ""
|
||||
|
||||
#. Label of the cost_allocation_per (Percent) field in DocType 'BOM Secondary
|
||||
#. Item'
|
||||
#: erpnext/manufacturing/doctype/bom_secondary_item/bom_secondary_item.json
|
||||
msgid "Cost Allocation %"
|
||||
msgstr "Распределение расходов %"
|
||||
msgstr ""
|
||||
|
||||
#. Label of the cost_allocation__process_loss_section (Section Break) field in
|
||||
#. DocType 'BOM'
|
||||
@@ -13453,7 +13453,7 @@ msgstr "Создать клиента"
|
||||
#: erpnext/selling/onboarding_step/create_delivery_note/create_delivery_note.json
|
||||
#: erpnext/stock/onboarding_step/create_delivery_note/create_delivery_note.json
|
||||
msgid "Create Delivery Note"
|
||||
msgstr "Создать транспортную накладную"
|
||||
msgstr "Создать транспортную накладную"
|
||||
|
||||
#: erpnext/stock/doctype/delivery_note/delivery_note_list.js:63
|
||||
msgid "Create Delivery Trip"
|
||||
@@ -22521,7 +22521,7 @@ msgstr "Получить комплектующие изделия"
|
||||
|
||||
#: erpnext/buying/doctype/supplier/supplier.js:151
|
||||
msgid "Get Supplier Group Details"
|
||||
msgstr "Получить данные о группе поставщиков"
|
||||
msgstr "Получить данные о группе поставщиков"
|
||||
|
||||
#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:461
|
||||
#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:481
|
||||
@@ -26685,7 +26685,7 @@ msgstr "После объединения позиций может потреб
|
||||
|
||||
#: erpnext/public/js/controllers/transaction.js:2569
|
||||
msgid "It is needed to fetch Item Details."
|
||||
msgstr "Это необходимо для отображения подробностей продукта."
|
||||
msgstr "Рто необходимо для отображения подробностей продукта."
|
||||
|
||||
#: banking/src/components/features/BankReconciliation/BankBalance.tsx:79
|
||||
msgid "It takes into account all the transactions that have been posted and subtracts the transactions that have not cleared yet."
|
||||
@@ -29337,7 +29337,7 @@ msgstr "Связь с клиентом не удалась. Пожалуйста
|
||||
|
||||
#: erpnext/selling/doctype/customer/customer.js:280
|
||||
msgid "Linking to Supplier Failed. Please try again."
|
||||
msgstr "Ссылка на поставщика не удалась. Попробуйте еще раз."
|
||||
msgstr "Ссылка на поставщика не удалась. Попробуйте еще раз."
|
||||
|
||||
#: erpnext/accounts/report/financial_ratios/financial_ratios.js:55
|
||||
#: erpnext/accounts/report/financial_ratios/financial_ratios.py:152
|
||||
@@ -31799,7 +31799,7 @@ msgstr ""
|
||||
|
||||
#: erpnext/selling/doctype/customer/customer.py:454
|
||||
msgid "Multiple Loyalty Programs found for Customer {}. Please select manually."
|
||||
msgstr "Найдено несколько программ лояльности для клиента {}. Выберите вручную."
|
||||
msgstr "Найдено несколько программ лояльности для клиента {}. Выберите вручную."
|
||||
|
||||
#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:1247
|
||||
msgid "Multiple POS Opening Entry"
|
||||
@@ -31807,7 +31807,7 @@ msgstr "Несколько записей открытия POS"
|
||||
|
||||
#: erpnext/accounts/doctype/pricing_rule/utils.py:348
|
||||
msgid "Multiple Price Rules exists with same criteria, please resolve conflict by assigning priority. Price Rules: {0}"
|
||||
msgstr "Несколько Цена Правила существует с теми же критериями, пожалуйста разрешить конфликт путем присвоения приоритета. Цена Правила: {0}"
|
||||
msgstr "Несколько Цена Правила существует с теми же критериями, пожалуйста разрешить конфликт путем присвоения приоритета. Цена Правила: {0}"
|
||||
|
||||
#. Option for the 'Loyalty Program Type' (Select) field in DocType 'Loyalty
|
||||
#. Program'
|
||||
@@ -37908,7 +37908,7 @@ msgstr ""
|
||||
|
||||
#: erpnext/public/js/utils/serial_no_batch_selector.js:662
|
||||
msgid "Please add atleast one Serial No / Batch No"
|
||||
msgstr "Пожалуйста, добавьте хотя бы один серийный номер/номер партии"
|
||||
msgstr "Пожалуйста, добавьте хотя бы один серийный номер/номер партии"
|
||||
|
||||
#: erpnext/crm/doctype/crm_settings/crm_settings.py:53
|
||||
msgid "Please add atleast one user on Allowed Users to allow Data Synchronization from Frappe CRM site."
|
||||
@@ -38496,7 +38496,7 @@ msgstr "Выберите счёт учёта товарных запасов"
|
||||
|
||||
#: erpnext/stock/doctype/stock_entry/stock_entry.py:2036
|
||||
msgid "Please select Subcontracting Order instead of Purchase Order {0}"
|
||||
msgstr "Пожалуйста, выберите «Заказ на субподряд» вместо «Заказ на закупку» {0}"
|
||||
msgstr "Пожалуйста, выберите «Заказ на субподряд» вместо «Заказ на закупку» {0}"
|
||||
|
||||
#: erpnext/controllers/accounts_controller.py:2857
|
||||
msgid "Please select Unrealized Profit / Loss account or add default Unrealized Profit / Loss account account for company {0}"
|
||||
@@ -38602,7 +38602,7 @@ msgstr "Пожалуйста, выберите поставщика для по
|
||||
|
||||
#: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py:142
|
||||
msgid "Please select a valid Purchase Order that has Service Items."
|
||||
msgstr "Пожалуйста, выберите действительный заказ на покупку, содержащий услуги."
|
||||
msgstr "Пожалуйста, выберите действительный заказ на покупку, содержащий услуги."
|
||||
|
||||
#: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py:139
|
||||
msgid "Please select a valid Purchase Order that is configured for Subcontracting."
|
||||
@@ -38852,7 +38852,7 @@ msgstr "Укажите компанию"
|
||||
|
||||
#: erpnext/assets/doctype/asset/asset.py:378
|
||||
msgid "Please set a Cost Center for the Asset or set an Asset Depreciation Cost Center for the Company {}"
|
||||
msgstr "Пожалуйста, установите Центр затрат для Актива или установите Центр затрат на амортизацию Актива для Компании {}"
|
||||
msgstr "Пожалуйста, установите Центр затрат для Актива или установите Центр затрат на амортизацию Актива для Компании {}"
|
||||
|
||||
#: erpnext/projects/doctype/project/project.py:768
|
||||
msgid "Please set a default Holiday List for Company {0}"
|
||||
@@ -38899,17 +38899,17 @@ msgstr "Пожалуйста, установите Cash умолчанию ил
|
||||
#: erpnext/accounts/doctype/pos_profile/pos_profile.py:197
|
||||
#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3097
|
||||
msgid "Please set default Cash or Bank account in Mode of Payment {}"
|
||||
msgstr "Установите по умолчанию наличный или банковский счет в режиме оплаты {}"
|
||||
msgstr "Установите по умолчанию наличный или банковский счет в режиме оплаты {}"
|
||||
|
||||
#: erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py:96
|
||||
#: erpnext/accounts/doctype/pos_profile/pos_profile.py:199
|
||||
#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:3099
|
||||
msgid "Please set default Cash or Bank account in Mode of Payments {}"
|
||||
msgstr "Установите по умолчанию наличный или банковский счет в режиме оплаты {}"
|
||||
msgstr "Установите по умолчанию наличный или банковский счет в режиме оплаты {}"
|
||||
|
||||
#: erpnext/accounts/utils.py:2522
|
||||
msgid "Please set default Exchange Gain/Loss Account in Company {}"
|
||||
msgstr "Пожалуйста, установите по умолчанию счет учета прибыли/убытка от курсовых разниц в компании {}"
|
||||
msgstr "Пожалуйста, установите по умолчанию счет учета прибыли/убытка от курсовых разниц в компании {}"
|
||||
|
||||
#: erpnext/assets/doctype/asset_repair/asset_repair.py:389
|
||||
msgid "Please set default Expense Account in Company {0}"
|
||||
@@ -39352,7 +39352,7 @@ msgstr "Время публикации"
|
||||
|
||||
#: erpnext/stock/doctype/stock_entry/stock_entry.py:2923
|
||||
msgid "Posting date and posting time is mandatory"
|
||||
msgstr "Дата публикации и размещения время является обязательным"
|
||||
msgstr "Дата публикации и размещения время является обязательным"
|
||||
|
||||
#: banking/src/components/features/BankReconciliation/MatchAndReconcile.tsx:901
|
||||
msgid "Posting date does not match the selected transaction"
|
||||
@@ -41466,7 +41466,7 @@ msgstr "Заказы на закупку для получения"
|
||||
|
||||
#: erpnext/controllers/accounts_controller.py:2048
|
||||
msgid "Purchase Orders {0} are un-linked"
|
||||
msgstr "Заказы на покупку {0} разъединены"
|
||||
msgstr "Заказы на покупку {0} разъединены"
|
||||
|
||||
#: erpnext/stock/report/item_prices/item_prices.py:59
|
||||
msgid "Purchase Price List"
|
||||
@@ -44208,7 +44208,7 @@ msgstr "Склад брака"
|
||||
|
||||
#: erpnext/public/js/utils/serial_no_batch_selector.js:670
|
||||
msgid "Rejected Warehouse and Accepted Warehouse cannot be same."
|
||||
msgstr "Склад отклоненных товаров и склад принятых товаров не могут быть одним и тем же."
|
||||
msgstr "Склад отклоненных товаров и склад принятых товаров не могут быть одним и тем же."
|
||||
|
||||
#: erpnext/buying/doctype/purchase_order/purchase_order_dashboard.py:23
|
||||
#: erpnext/buying/doctype/supplier_quotation/supplier_quotation_dashboard.py:14
|
||||
@@ -46864,7 +46864,7 @@ msgstr ""
|
||||
|
||||
#: erpnext/assets/doctype/asset/asset.py:425
|
||||
msgid "Row #{}: Finance Book should not be empty since you're using multiple."
|
||||
msgstr "Строка #{}: Финансовая книга не может быть пустой, так как используется несколько книг."
|
||||
msgstr "Строка #{}: Финансовая книга не может быть пустой, так как используется несколько книг."
|
||||
|
||||
#: erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py:92
|
||||
msgid "Row #{}: POS Invoice {} has been {}"
|
||||
@@ -46888,7 +46888,7 @@ msgstr "Строка №{}: Назначьте задачу участнику."
|
||||
|
||||
#: erpnext/assets/doctype/asset/asset.py:417
|
||||
msgid "Row #{}: Please use a different Finance Book."
|
||||
msgstr "Строка #{}: Используйте другую финансовую книгу."
|
||||
msgstr "Строка #{}: Рспользуйте РґСЂСѓРіСѓСЋ финансовую РєРЅРёРіСѓ."
|
||||
|
||||
#: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:524
|
||||
msgid "Row #{}: Serial No {} cannot be returned since it was not transacted in original invoice {}"
|
||||
@@ -46900,7 +46900,7 @@ msgstr "Строка #{}: Исходный счёт {} возвратного с
|
||||
|
||||
#: erpnext/accounts/doctype/pos_invoice/pos_invoice.py:497
|
||||
msgid "Row #{}: You cannot add positive quantities in a return invoice. Please remove item {} to complete the return."
|
||||
msgstr "Строка #{}: Вы не можете добавлять положительные количества в счет-фактуру возврата. Пожалуйста, удалите элемент {}, чтобы завершить возврат."
|
||||
msgstr "Строка #{}: Вы не можете добавлять положительные количества в счет-фактуру возврата. Пожалуйста, удалите элемент {}, чтобы завершить возврат."
|
||||
|
||||
#: erpnext/stock/doctype/pick_list/pick_list.py:239
|
||||
msgid "Row #{}: item {} has been picked already."
|
||||
@@ -46917,7 +46917,7 @@ msgstr ""
|
||||
|
||||
#: erpnext/stock/doctype/item/item.py:1537
|
||||
msgid "Row #{}: {} {} doesn't belong to Company {}. Please select valid {}."
|
||||
msgstr "Строка №{}: {} {} не принадлежит компании {}. Выберите допустимый {}."
|
||||
msgstr "Строка №{}: {} {} не принадлежит компании {}. Выберите допустимый {}."
|
||||
|
||||
#: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py:450
|
||||
msgid "Row No {0}: Warehouse is required. Please set a Default Warehouse for Item {1} and Company {2}"
|
||||
@@ -47147,7 +47147,7 @@ msgstr "Строка {0}: Выберите активную спецификац
|
||||
|
||||
#: erpnext/controllers/subcontracting_controller.py:224
|
||||
msgid "Row {0}: Please select an valid BOM for Item {1}."
|
||||
msgstr "Строка {0}: Выберите действительную спецификацию для товара {1}."
|
||||
msgstr "Строка {0}: Выберите действительную спецификацию для товара {1}."
|
||||
|
||||
#: erpnext/regional/italy/utils.py:290
|
||||
msgid "Row {0}: Please set at Tax Exemption Reason in Sales Taxes and Charges"
|
||||
@@ -47322,7 +47322,7 @@ msgstr "В строках {0} указан тип ссылки 'Платежна
|
||||
|
||||
#: erpnext/controllers/accounts_controller.py:307
|
||||
msgid "Rows: {0} in {1} section are Invalid. Reference Name should point to a valid Payment Entry or Journal Entry."
|
||||
msgstr "Строки: {0} в разделе {1} недействительны. Имя ссылки должно указывать на действительную запись платежа или запись журнала."
|
||||
msgstr "Строки: {0} РІ разделе {1} недействительны. РРјСЏ ссылки должно указывать РЅР° действительную запись платежа или запись журнала."
|
||||
|
||||
#. Label of the rule_applied (Check) field in DocType 'Pricing Rule Detail'
|
||||
#: erpnext/accounts/doctype/pricing_rule_detail/pricing_rule_detail.json
|
||||
@@ -51972,7 +51972,7 @@ msgstr "Подробности о запасах"
|
||||
|
||||
#: erpnext/stock/doctype/stock_entry/stock_entry.py:1210
|
||||
msgid "Stock Entries already created for Work Order {0}: {1}"
|
||||
msgstr "Записи по запасам уже созданы для заказа на работу {0}: {1}"
|
||||
msgstr "Записи по запасам уже созданы для заказа на работу {0}: {1}"
|
||||
|
||||
#. Label of the stock_entry (Link) field in DocType 'Journal Entry'
|
||||
#. Label of a Link in the Manufacturing Workspace
|
||||
@@ -52036,7 +52036,7 @@ msgstr "Создана складская запись {0}"
|
||||
|
||||
#: erpnext/manufacturing/doctype/job_card/job_card.py:1614
|
||||
msgid "Stock Entry {0} has created"
|
||||
msgstr "Запись по запасам {0} была создана"
|
||||
msgstr "Запись по запасам {0} была создана"
|
||||
|
||||
#: erpnext/accounts/doctype/journal_entry/journal_entry.py:1335
|
||||
msgid "Stock Entry {0} is not submitted"
|
||||
@@ -52679,7 +52679,7 @@ msgstr ""
|
||||
|
||||
#: erpnext/selling/page/point_of_sale/pos_controller.js:826
|
||||
msgid "Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2} {3}."
|
||||
msgstr "Количество на складе недостаточно для Код товара: {0} на складе {1}. Доступное количество {2} {3}."
|
||||
msgstr "Количество на складе недостаточно для Код товара: {0} на складе {1}. Доступное количество {2} {3}."
|
||||
|
||||
#: erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py:256
|
||||
msgid "Stock transactions before {0} are frozen"
|
||||
@@ -55332,7 +55332,7 @@ msgstr "Список выбора, имеющий записи резервир
|
||||
|
||||
#: erpnext/stock/doctype/stock_entry/stock_entry.py:3208
|
||||
msgid "The Process Loss Qty has reset as per job cards Process Loss Qty"
|
||||
msgstr "Количество потерь в процессе было сброшено в соответствии с количеством потерь в карточках рабочих заданий"
|
||||
msgstr "Количество потерь в процессе было сброшено в соответствии с количеством потерь в карточках рабочих заданий"
|
||||
|
||||
#: erpnext/setup/doctype/sales_person/sales_person.py:102
|
||||
msgid "The Sales Person is linked with {0}"
|
||||
@@ -55720,7 +55720,7 @@ msgstr "Акций не существует с {0}"
|
||||
|
||||
#: erpnext/stock/stock_ledger.py:866
|
||||
msgid "The stock for the item {0} in the {1} warehouse was negative on the {2}. You should create a positive entry {3} before the date {4} and time {5} to post the correct valuation rate. For more details, please read the <a href='https://docs.erpnext.com/docs/user/manual/en/stock-adjustment-cogs-with-negative-stock'>documentation<a>."
|
||||
msgstr "Запас товара {0} на складе {1} был отрицательным на {2}. Вам нужно создать положительную запись {3} до даты {4} и времени {5}, чтобы корректно зафиксировать стоимость. Для получения подробной информации, пожалуйста, прочитайте <a href='https://docs.erpnext.com/docs/user/manual/en/stock-adjustment-cogs-with-negative-stock'>документацию<a>."
|
||||
msgstr "Запас товара {0} на складе {1} был отрицательным на {2}. Вам нужно создать положительную запись {3} до даты {4} и времени {5}, чтобы корректно зафиксировать стоимость. Для получения подробной информации, пожалуйста, прочитайте <a href='https://docs.erpnext.com/docs/user/manual/en/stock-adjustment-cogs-with-negative-stock'>документацию<a>."
|
||||
|
||||
#: erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py:742
|
||||
msgid "The stock has been reserved for the following Items and Warehouses, un-reserve the same to {0} the Stock Reconciliation: <br /><br /> {1}"
|
||||
@@ -55754,7 +55754,7 @@ msgstr "Задача поставлена в очередь как фоново
|
||||
|
||||
#: erpnext/stock/doctype/material_request/material_request.py:400
|
||||
msgid "The total Issue / Transfer quantity {0} in Material Request {1} cannot be greater than allowed requested quantity {2} for Item {3}"
|
||||
msgstr "Общее количество выпуска/передачи {0} в запросе на материалы {1} не может быть больше, чем допустимое запрошенное количество {2} для товара {3}"
|
||||
msgstr "Общее количество выпуска/передачи {0} в запросе на материалы {1} не может быть больше, чем допустимое запрошенное количество {2} для товара {3}"
|
||||
|
||||
#: erpnext/stock/doctype/material_request/material_request.py:407
|
||||
msgid "The total Issue / Transfer quantity {0} in Material Request {1} cannot be greater than requested quantity {2} for Item {3}"
|
||||
@@ -55883,7 +55883,7 @@ msgstr ""
|
||||
|
||||
#: erpnext/stock/doctype/item/item.js:1296
|
||||
msgid "There are two options to maintain valuation of stock. FIFO (first in - first out) and Moving Average. To understand this topic in detail please visit <a href='https://docs.frappe.io/erpnext/user/manual/en/calculation-of-valuation-rate-in-fifo-and-moving-average' target='_blank'>Item Valuation, FIFO and Moving Average.</a>"
|
||||
msgstr "Существует два варианта ведения оценки запасов. FIFO (первым пришел - первым ушел) и скользящая средняя. Чтобы подробно разобраться в этой теме, посетите <a href='https://docs.frappe.io/erpnext/user/manual/en/calculation-of-valuation-rate-in-fifo-and-moving-average' target='_blank'>Оценка товара, FIFO и скользящая средняя.</a>"
|
||||
msgstr "Существует два варианта ведения оценки запасов. FIFO (первым пришел - первым ушел) и скользящая средняя. Чтобы подробно разобраться в этой теме, посетите <a href='https://docs.frappe.io/erpnext/user/manual/en/calculation-of-valuation-rate-in-fifo-and-moving-average' target='_blank'>Оценка товара, FIFO и скользящая средняя.</a>"
|
||||
|
||||
#: banking/src/components/features/BankReconciliation/MatchAndReconcile.tsx:982
|
||||
msgid "There are {0} unreconciled transactions before {1}."
|
||||
@@ -55923,7 +55923,7 @@ msgstr ""
|
||||
|
||||
#: erpnext/stock/doctype/stock_entry/stock_entry.py:2111
|
||||
msgid "There must be atleast 1 Finished Good in this Stock Entry"
|
||||
msgstr "В этой записи о движении товаров должно быть хотя бы одно готовое изделие"
|
||||
msgstr "В этой записи о движении товаров должно быть хотя бы одно готовое изделие"
|
||||
|
||||
#: erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py:153
|
||||
msgid "There was an error creating Bank Account while linking with Plaid."
|
||||
@@ -56344,7 +56344,7 @@ msgstr ""
|
||||
|
||||
#: erpnext/controllers/selling_controller.py:886
|
||||
msgid "This {} will be treated as material transfer."
|
||||
msgstr "Это {} будет рассматриваться как передача материала."
|
||||
msgstr "Рто {} будет рассматриваться как передача материала."
|
||||
|
||||
#. Option for the 'Under Withheld Reason' (Select) field in DocType 'Tax
|
||||
#. Withholding Entry'
|
||||
@@ -62032,7 +62032,7 @@ msgstr "Вы можете использовать {0} для сверки с {1
|
||||
|
||||
#: erpnext/manufacturing/doctype/job_card/job_card.py:1391
|
||||
msgid "You can't make any changes to Job Card since Work Order is closed."
|
||||
msgstr "Вы не можете вносить изменения в Карту работы, поскольку Заказ на работу закрыт."
|
||||
msgstr "Вы не можете вносить изменения в Карту работы, поскольку Заказ на работу закрыт."
|
||||
|
||||
#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:230
|
||||
msgid "You can't process the serial number {0} as it has already been used in the SABB {1}. {2} if you want to inward same serial number multiple times then enabled 'Allow existing Serial No to be Manufactured/Received again' in the {3}"
|
||||
@@ -62056,7 +62056,7 @@ msgstr ""
|
||||
|
||||
#: erpnext/accounts/general_ledger.py:851
|
||||
msgid "You cannot create/amend any accounting entries till this date."
|
||||
msgstr "Создание и изменение бухгалтерских записей невозможно до указанной даты."
|
||||
msgstr "Создание и изменение бухгалтерских записей невозможно до указанной даты."
|
||||
|
||||
#: erpnext/accounts/doctype/journal_entry/journal_entry.py:961
|
||||
msgid "You cannot credit and debit same account at the same time"
|
||||
@@ -62084,7 +62084,7 @@ msgstr "Вы не можете обменять более {0}."
|
||||
|
||||
#: erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py:211
|
||||
msgid "You cannot repost item valuation before {}"
|
||||
msgstr "Невозможно повторно провести оценку стоимости товара до {}"
|
||||
msgstr "Невозможно повторно провести оценку стоимости товара до {}"
|
||||
|
||||
#: erpnext/accounts/doctype/subscription/subscription.py:766
|
||||
msgid "You cannot restart a Subscription that is not cancelled."
|
||||
@@ -62918,7 +62918,7 @@ msgstr "{0} не включен в {1}"
|
||||
|
||||
#: erpnext/setup/doctype/transaction_deletion_record/transaction_deletion_record.py:647
|
||||
msgid "{0} is not running. Cannot trigger events for this Document"
|
||||
msgstr "{0} не запущен. Невозможно запустить события для этого документа"
|
||||
msgstr "{0} не запущен. Невозможно запустить события для этого документа"
|
||||
|
||||
#: erpnext/stock/doctype/material_request/material_request.py:835
|
||||
msgid "{0} is not the default supplier for any items."
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-08-09 09:47+0000\n"
|
||||
"PO-Revision-Date: 2026-08-26 11:43\n"
|
||||
"PO-Revision-Date: 2026-08-09 11:02\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Slovenian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-08-09 09:47+0000\n"
|
||||
"PO-Revision-Date: 2026-08-26 11:43\n"
|
||||
"PO-Revision-Date: 2026-08-09 11:02\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Serbian (Cyrillic)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-08-09 09:47+0000\n"
|
||||
"PO-Revision-Date: 2026-08-26 11:43\n"
|
||||
"PO-Revision-Date: 2026-08-09 11:03\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Serbian (Latin)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-08-09 09:47+0000\n"
|
||||
"PO-Revision-Date: 2026-08-26 11:43\n"
|
||||
"PO-Revision-Date: 2026-08-10 11:05\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Swedish\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -740,7 +740,7 @@ msgid "<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>"
|
||||
msgstr "<h3>Växelkurs Inställningar Hjälp</h3>\n"
|
||||
msgstr "<h3>Valutaväxling Inställningar Hjälp</h3>\n"
|
||||
"<p>Det finns 3 variabler som kan användas av slutpunkt, resultat nyckel och i parameter värde.</p>\n"
|
||||
"<p>Växelkurs mellan {from_currency} och {to_currency} {transaction_date} hämtas av API.</p>\n"
|
||||
"<p>Exempel: Om slutpunkt är exchange.com/2021-08-01 måste du ange exchange.com/{transaction_date}</p>"
|
||||
@@ -1079,7 +1079,7 @@ msgstr "Helg Lista kan läggas till för att utesluta dessa dagar för Arbetssta
|
||||
|
||||
#: erpnext/crm/doctype/lead/lead.py:142
|
||||
msgid "A Lead requires either a person's name or an organization's name"
|
||||
msgstr "Potentiell Kund erfordrar antingen person namn eller bolag namn"
|
||||
msgstr "Potentiell Kund kräver antingen person namn eller bolag namn"
|
||||
|
||||
#: erpnext/stock/doctype/packing_slip/packing_slip.py:84
|
||||
msgid "A Packing Slip can only be created for Draft Delivery Note."
|
||||
@@ -1229,7 +1229,7 @@ msgstr "Service Avtal Utgång Datum"
|
||||
#. Label of a Workspace Sidebar Item
|
||||
#: erpnext/workspace_sidebar/financial_reports.json
|
||||
msgid "AP Summary"
|
||||
msgstr "Skuldöversikt"
|
||||
msgstr "Skulder Översikt"
|
||||
|
||||
#. Label of the api_details_section (Section Break) field in DocType 'Currency
|
||||
#. Exchange Settings'
|
||||
@@ -1240,7 +1240,7 @@ msgstr "API Detaljer"
|
||||
#. Label of a Workspace Sidebar Item
|
||||
#: erpnext/workspace_sidebar/financial_reports.json
|
||||
msgid "AR Summary"
|
||||
msgstr "Fordringöversikt"
|
||||
msgstr "Fordringar Översikt"
|
||||
|
||||
#. Label of the awb_number (Data) field in DocType 'Shipment'
|
||||
#: erpnext/stock/doctype/shipment/shipment.json
|
||||
@@ -2254,7 +2254,7 @@ msgstr "Fordring Rabatt Konto"
|
||||
#: erpnext/accounts/report/accounts_receivable/accounts_receivable.js:205
|
||||
#: erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.json
|
||||
msgid "Accounts Receivable Summary"
|
||||
msgstr "Fordringöversikt"
|
||||
msgstr "Fordringar Översikt"
|
||||
|
||||
#. Label of the accounts_receivable_unpaid (Link) field in DocType 'Invoice
|
||||
#. Discounting'
|
||||
@@ -5232,7 +5232,7 @@ msgstr "Tillämpad Dimension"
|
||||
#. Description of the 'Holiday List' (Link) field in DocType 'Employee'
|
||||
#: erpnext/setup/doctype/employee/employee.json
|
||||
msgid "Applicable Holiday List"
|
||||
msgstr "Tillämplig Helg Lista"
|
||||
msgstr "Tillämpligt Helg Lista"
|
||||
|
||||
#. Label of the applicable_modules_section (Section Break) field in DocType
|
||||
#. 'Terms and Conditions'
|
||||
@@ -5250,22 +5250,22 @@ msgstr "Tillämplig På Konto"
|
||||
#. Label of the to_designation (Link) field in DocType 'Authorization Rule'
|
||||
#: erpnext/setup/doctype/authorization_rule/authorization_rule.json
|
||||
msgid "Applicable To (Designation)"
|
||||
msgstr "Tillämplig för (Befattning)"
|
||||
msgstr "Tillämpligt för (Befattning)"
|
||||
|
||||
#. Label of the to_emp (Link) field in DocType 'Authorization Rule'
|
||||
#: erpnext/setup/doctype/authorization_rule/authorization_rule.json
|
||||
msgid "Applicable To (Employee)"
|
||||
msgstr "Tillämplig för (Personal)"
|
||||
msgstr "Tillämpligt för (Personal)"
|
||||
|
||||
#. Label of the system_role (Link) field in DocType 'Authorization Rule'
|
||||
#: erpnext/setup/doctype/authorization_rule/authorization_rule.json
|
||||
msgid "Applicable To (Role)"
|
||||
msgstr "Tillämplig för (Roll)"
|
||||
msgstr "Tillämpligt för (Roll)"
|
||||
|
||||
#. Label of the system_user (Link) field in DocType 'Authorization Rule'
|
||||
#: erpnext/setup/doctype/authorization_rule/authorization_rule.json
|
||||
msgid "Applicable To (User)"
|
||||
msgstr "Tillämplig för (Användare)"
|
||||
msgstr "Tillämpligt för (Användare)"
|
||||
|
||||
#. Label of the countries (Table) field in DocType 'Price List'
|
||||
#: erpnext/stock/doctype/price_list/price_list.json
|
||||
@@ -5281,19 +5281,19 @@ msgstr "Användare"
|
||||
#. Description of the 'Transporter' (Link) field in DocType 'Driver'
|
||||
#: erpnext/setup/doctype/driver/driver.json
|
||||
msgid "Applicable for external driver"
|
||||
msgstr "Tillämplig för extern Förare"
|
||||
msgstr "Tillämpligt för extern Förare"
|
||||
|
||||
#: erpnext/regional/italy/setup.py:162
|
||||
msgid "Applicable if the company is SpA, SApA or SRL"
|
||||
msgstr "Tillämplig om bolag är SpA, SApA eller SRL"
|
||||
msgstr "Tillämpligt om bolag är SpA, SApA eller SRL"
|
||||
|
||||
#: erpnext/regional/italy/setup.py:171
|
||||
msgid "Applicable if the company is a limited liability company"
|
||||
msgstr "Tillämplig om bolag är Aktie Bolag"
|
||||
msgstr "Tillämpligt om bolag är Aktie Bolag"
|
||||
|
||||
#: erpnext/regional/italy/setup.py:122
|
||||
msgid "Applicable if the company is an Individual or a Proprietorship"
|
||||
msgstr "Tillämplig om bolag är en individ eller ett Privat Bolag"
|
||||
msgstr "Tillämpligt om bolag är en individ eller ett Privat Bolag"
|
||||
|
||||
#. Label of the applicable_on_cumulative_expense (Check) field in DocType
|
||||
#. 'Budget'
|
||||
@@ -5305,7 +5305,7 @@ msgstr "Tillämplig på Ackumulerad Kostnad"
|
||||
#. 'Budget'
|
||||
#: erpnext/accounts/doctype/budget/budget.json
|
||||
msgid "Applicable on Material Request"
|
||||
msgstr "Tillämplig på Material Begäran"
|
||||
msgstr "Tillämpligt på Material Begäran"
|
||||
|
||||
#. Description of the 'Allow Partial Payment' (Check) field in DocType 'POS
|
||||
#. Profile'
|
||||
@@ -5316,13 +5316,13 @@ msgstr "Tillämplig på Kassa Faktura"
|
||||
#. Label of the applicable_on_purchase_order (Check) field in DocType 'Budget'
|
||||
#: erpnext/accounts/doctype/budget/budget.json
|
||||
msgid "Applicable on Purchase Order"
|
||||
msgstr "Tillämplig på Inköp Order"
|
||||
msgstr "Tillämpligt på Inköp Order"
|
||||
|
||||
#. Label of the applicable_on_booking_actual_expenses (Check) field in DocType
|
||||
#. 'Budget'
|
||||
#: erpnext/accounts/doctype/budget/budget.json
|
||||
msgid "Applicable on booking actual expenses"
|
||||
msgstr "Tillämplig vid Bokföring av Faktiska Kostnader"
|
||||
msgstr "Tillämpligt vid Bokföring av Faktiska Kostnader"
|
||||
|
||||
#: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py:10
|
||||
#: erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py:10
|
||||
@@ -5532,7 +5532,7 @@ msgstr "Tid Bokning Bekräftelse"
|
||||
|
||||
#: erpnext/crm/doctype/appointment/appointment.py:189
|
||||
msgid "Appointment Confirmed"
|
||||
msgstr "Tid Bokning Bekräftad"
|
||||
msgstr "Tidsbokning Bekräftad"
|
||||
|
||||
#: erpnext/www/book_appointment/index.js:237
|
||||
msgid "Appointment Created Successfully"
|
||||
@@ -5554,7 +5554,7 @@ msgstr "Tid Bokning Varar (Minuter)"
|
||||
#. 'Appointment Booking Settings'
|
||||
#: erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json
|
||||
msgid "Appointment Scheduling"
|
||||
msgstr "Tid Bokning Schemaläggning"
|
||||
msgstr "Tidsbokning Schemaläggning"
|
||||
|
||||
#: erpnext/www/book_appointment/index.py:24
|
||||
msgid "Appointment Scheduling Disabled"
|
||||
@@ -5566,7 +5566,7 @@ msgstr "Tid Bokning är Inaktiverad för denna Webbplats"
|
||||
|
||||
#: erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py:101
|
||||
msgid "Appointment Scheduling needs to be enabled for Appointment Booking through portal."
|
||||
msgstr "Tid Bokning Schemaläggning måste vara aktiverad för Tid Bokning via portal."
|
||||
msgstr "Tidsbokning Schemaläggning måste vara aktiverad för Tidsbokning via portal."
|
||||
|
||||
#. Label of the appointment_with (Link) field in DocType 'Appointment'
|
||||
#: erpnext/crm/doctype/appointment/appointment.json
|
||||
@@ -5575,31 +5575,31 @@ msgstr "Tid Bokning med"
|
||||
|
||||
#: erpnext/crm/doctype/appointment/appointment.py:86
|
||||
msgid "Appointment can only be scheduled up to {0} day(s) in advance."
|
||||
msgstr "Tid Bokning kan endast schemaläggas upp till {0} dag(ar) i förväg."
|
||||
msgstr "Tidsbokning kan endast schemaläggas upp till {0} dag(ar) i förväg."
|
||||
|
||||
#: erpnext/crm/doctype/appointment/appointment.py:79
|
||||
msgid "Appointment cannot be scheduled for a past time."
|
||||
msgstr "Tid Bokning kan inte schemaläggas för förfluten tid."
|
||||
msgstr "Tidsbokning kan inte schemaläggas för förfluten tid."
|
||||
|
||||
#: erpnext/crm/doctype/appointment/appointment.py:98
|
||||
msgid "Appointment cannot be scheduled on a holiday."
|
||||
msgstr "Tid Bokning kan inte schemaläggas på helgdag."
|
||||
msgstr "Tidsbokning kan inte schemaläggas på helgdag."
|
||||
|
||||
#: erpnext/www/book_appointment/verify/index.py:28
|
||||
msgid "Appointment has been closed. Please book the appointment again."
|
||||
msgstr "Tid Bokning har stängts. Boka igen."
|
||||
msgstr "Tidsbokning har stängts. Boka igen."
|
||||
|
||||
#: erpnext/www/book_appointment/verify/index.py:33
|
||||
msgid "Appointment is already verified."
|
||||
msgstr "Tid Bokning är redan bekräftad."
|
||||
msgstr "Tidsbokning är redan bekräftad."
|
||||
|
||||
#: erpnext/crm/doctype/appointment/appointment.py:116
|
||||
msgid "Appointment must be scheduled within the available slot timings."
|
||||
msgstr "Tid Bokning måste schemaläggas inom tillgänglig tidsintervall."
|
||||
msgstr "Tidsbokning måste schemaläggas inom tillgänglig tidsintervall."
|
||||
|
||||
#: erpnext/crm/doctype/appointment/appointment.py:66
|
||||
msgid "Appointments created manually cannot have 'Unverified' status."
|
||||
msgstr "Tid Bokningar som skapas manuellt kan inte ha ”Overifierad” status."
|
||||
msgstr "Tidsbokningar som skapas manuellt kan inte ha ”Overifierad” status."
|
||||
|
||||
#. Label of the approving_role (Link) field in DocType 'Authorization Rule'
|
||||
#: erpnext/setup/doctype/authorization_rule/authorization_rule.json
|
||||
@@ -6349,7 +6349,7 @@ msgstr "På Rad {0}: I Serie och Parti Paket {1} måste dokument status vara 1 o
|
||||
|
||||
#: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:84
|
||||
msgid "At least one account with exchange gain or loss is required"
|
||||
msgstr "Minst ett konto med Växelkurs Resultat erfordras"
|
||||
msgstr "Minst ett konto med Valutaväxling Resultat erfordras"
|
||||
|
||||
#: erpnext/assets/doctype/asset/asset.py:1293
|
||||
msgid "At least one asset has to be selected."
|
||||
@@ -9884,7 +9884,7 @@ msgstr "Kan inte dra av när an kategori \"Värdering\" eller \"Värdering och T
|
||||
|
||||
#: erpnext/accounts/doctype/payment_entry/payment_entry.js:1854
|
||||
msgid "Cannot delete Exchange Gain/Loss row"
|
||||
msgstr "Kan inte ta bort Växelkurs Resultat rad"
|
||||
msgstr "Kan inte ta bort Valutaväxling Resultat rad"
|
||||
|
||||
#: erpnext/stock/doctype/serial_no/serial_no.py:120
|
||||
msgid "Cannot delete Serial No {0}, as it is used in stock transactions"
|
||||
@@ -14325,7 +14325,7 @@ msgstr "Cup"
|
||||
#: erpnext/setup/doctype/currency_exchange/currency_exchange.json
|
||||
#: erpnext/workspace_sidebar/accounts_setup.json
|
||||
msgid "Currency Exchange"
|
||||
msgstr "Växelkurs"
|
||||
msgstr "Valutaväxling"
|
||||
|
||||
#. Label of the currency_exchange_section (Section Break) field in DocType
|
||||
#. 'Accounts Settings'
|
||||
@@ -14336,21 +14336,21 @@ msgstr "Växelkurs"
|
||||
#: erpnext/workspace_sidebar/accounts_setup.json
|
||||
#: erpnext/workspace_sidebar/erpnext_settings.json
|
||||
msgid "Currency Exchange Settings"
|
||||
msgstr "Växelkurs Inställningar"
|
||||
msgstr "Valutaväxling Inställningar"
|
||||
|
||||
#. Name of a DocType
|
||||
#: erpnext/accounts/doctype/currency_exchange_settings_details/currency_exchange_settings_details.json
|
||||
msgid "Currency Exchange Settings Details"
|
||||
msgstr "Växelkurs Inställning Detaljer"
|
||||
msgstr "Valutaväxling Inställning Detaljer"
|
||||
|
||||
#. Name of a DocType
|
||||
#: erpnext/accounts/doctype/currency_exchange_settings_result/currency_exchange_settings_result.json
|
||||
msgid "Currency Exchange Settings Result"
|
||||
msgstr "Växelkurs Inställning Resultat"
|
||||
msgstr "Valutaväxling Inställning Resultat"
|
||||
|
||||
#: erpnext/setup/doctype/currency_exchange/currency_exchange.py:55
|
||||
msgid "Currency Exchange must be applicable for Buying or for Selling."
|
||||
msgstr "Växelkurs måste vara tillämplig för Inköp eller Försäljning."
|
||||
msgstr "Valutaväxling måste vara tillämplig för Inköp eller Försäljning."
|
||||
|
||||
#. Label of the currency_and_price_list (Section Break) field in DocType 'POS
|
||||
#. Invoice'
|
||||
@@ -18183,7 +18183,7 @@ msgstr "Driftstopp Tid (Timmar)"
|
||||
#: erpnext/manufacturing/workspace/manufacturing/manufacturing.json
|
||||
#: erpnext/workspace_sidebar/manufacturing.json
|
||||
msgid "Downtime Analysis"
|
||||
msgstr "Driftstopp Statistik"
|
||||
msgstr "Driftstopp Analys"
|
||||
|
||||
#. Name of a DocType
|
||||
#. Label of a Link in the Manufacturing Workspace
|
||||
@@ -19009,7 +19009,7 @@ msgstr "Aktivera Tillåt Partiell Reservation i Lager Inställningar för att re
|
||||
#. Booking Settings'
|
||||
#: erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json
|
||||
msgid "Enable Appointment Booking Through Portal"
|
||||
msgstr "Aktivera Tid Bokning via Portal"
|
||||
msgstr "Aktivera Tidsbokning via Portal"
|
||||
|
||||
#. Label of the enable_scheduling (Check) field in DocType 'Appointment Booking
|
||||
#. Settings'
|
||||
@@ -19280,7 +19280,7 @@ msgstr "Aktivera för att göra denna leverantör valbar som transportör på F
|
||||
#. Description of the 'Retain Sample' (Check) field in DocType 'Item'
|
||||
#: erpnext/stock/doctype/item/item.json
|
||||
msgid "Enable to reserve a small sample from each batch for any analysis arising ahead"
|
||||
msgstr "Aktivera för att reservera litet prov från varje parti för statistik som uppstår senare"
|
||||
msgstr "Aktivera för att reservera litet prov från varje parti för analys som uppstår senare"
|
||||
|
||||
#. Label of the enable_tracking_sales_commissions (Check) field in DocType
|
||||
#. 'Selling Settings'
|
||||
@@ -19752,17 +19752,17 @@ msgstr "Inställning"
|
||||
#. 'Company'
|
||||
#: erpnext/setup/doctype/company/company.json
|
||||
msgid "Exchange Gain / Loss"
|
||||
msgstr "Växelkurs Resultat"
|
||||
msgstr "Valutaväxling Resultat"
|
||||
|
||||
#. Label of the exchange_gain_loss_account (Link) field in DocType 'Company'
|
||||
#: erpnext/setup/doctype/company/company.json
|
||||
msgid "Exchange Gain / Loss Account"
|
||||
msgstr "Växelkurs Resultat Konto"
|
||||
msgstr "Valutaväxling Resultat Konto"
|
||||
|
||||
#. Option for the 'Entry Type' (Select) field in DocType 'Journal Entry'
|
||||
#: erpnext/accounts/doctype/journal_entry/journal_entry.json
|
||||
msgid "Exchange Gain Or Loss"
|
||||
msgstr "Växelkurs Resultat"
|
||||
msgstr "Valutaväxling Resultat"
|
||||
|
||||
#. Label of the exchange_gain_loss (Currency) field in DocType 'Payment Entry
|
||||
#. Reference'
|
||||
@@ -19777,12 +19777,12 @@ msgstr "Växelkurs Resultat"
|
||||
#: erpnext/accounts/doctype/sales_invoice_advance/sales_invoice_advance.json
|
||||
#: erpnext/setup/doctype/company/company.py:677
|
||||
msgid "Exchange Gain/Loss"
|
||||
msgstr "Växelkurs Resultat"
|
||||
msgstr "Valutaväxling Resultat"
|
||||
|
||||
#: erpnext/controllers/accounts_controller.py:1809
|
||||
#: erpnext/controllers/accounts_controller.py:1894
|
||||
msgid "Exchange Gain/Loss amount has been booked through {0}"
|
||||
msgstr "Växelkurs Resultat Belopp har bokförts genom {0}"
|
||||
msgstr "Valutaväxling Resultat Belopp har bokförts genom {0}"
|
||||
|
||||
#. Label of the exchange_rate (Float) field in DocType 'Advance Payment Ledger
|
||||
#. Entry'
|
||||
@@ -23282,7 +23282,7 @@ msgstr "Här kan du välja överordnade för Personal. Baserat på detta kommer
|
||||
|
||||
#: erpnext/setup/doctype/holiday_list/holiday_list.js:77
|
||||
msgid "Here, your weekly offs are pre-populated based on the previous selections. You can add more rows to also add public and national holidays individually."
|
||||
msgstr "Här är dina veckofrånvaro förifyllda baserat på tidigare val. Du kan lägga till fler rader för att även lägga till allmänna och nationella helgdagar individuellt."
|
||||
msgstr "Här är dina veckoledigheter förifyllda baserat på tidigare val. Du kan lägga till fler rader för att även lägga till allmänna och nationella helgdagar individuellt."
|
||||
|
||||
#. Name of a UOM
|
||||
#: erpnext/setup/setup_wizard/data/uom_data.json
|
||||
@@ -23725,7 +23725,7 @@ msgstr "Om aktiverad, kommer utskrift av detta dokument att bifogas till varje e
|
||||
#. (Check) field in DocType 'Stock Reposting Settings'
|
||||
#: erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json
|
||||
msgid "If enabled, a weekly scheduler scans the Stock Ledger Variance for item-warehouses with incorrect valuation in the current financial year and auto-creates Item & Warehouse based reposts to fix them."
|
||||
msgstr "Om aktiverad, veckovis schemaläggare skannar Lager Register Avvikelse efter lager artikel med felaktig värdering under innevarande bokföring år och automatiskt skapar Artikel & Lager baserade ombokningar för att fixa dem."
|
||||
msgstr "Om aktiverad, en veckovis schemaläggare skannar Lager Register Avvikelse efter lager artikel med felaktig värdering under innevarande bokföring år och automatiskt skapar Artikel & Lager baserade ombokningar för att fixa dem."
|
||||
|
||||
#. Description of the 'Enable discount accounting for selling' (Check) field in
|
||||
#. DocType 'Selling Settings'
|
||||
@@ -26208,7 +26208,7 @@ msgstr "Är Rabatterad"
|
||||
#. Deduction'
|
||||
#: erpnext/accounts/doctype/payment_entry_deduction/payment_entry_deduction.json
|
||||
msgid "Is Exchange Gain / Loss?"
|
||||
msgstr "Är Växelkurs Resultat?"
|
||||
msgstr "Är Valutaväxling Resultat?"
|
||||
|
||||
#. Label of the is_expandable (Check) field in DocType 'BOM Creator Item'
|
||||
#: erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json
|
||||
@@ -29270,7 +29270,7 @@ msgstr "Begränsad till tolv tecken"
|
||||
#. Settings'
|
||||
#: erpnext/stock/doctype/stock_reposting_settings/stock_reposting_settings.json
|
||||
msgid "Limits don't apply on"
|
||||
msgstr "Begränsning gäller inte för"
|
||||
msgstr "Begränsningar gäller inte för"
|
||||
|
||||
#. Label of the reference_code (Data) field in DocType 'Financial Report Row'
|
||||
#: erpnext/accounts/doctype/financial_report_row/financial_report_row.json
|
||||
@@ -32703,7 +32703,7 @@ msgstr "Inga extra fält tillgängliga"
|
||||
|
||||
#: erpnext/crm/doctype/appointment/appointment.py:103
|
||||
msgid "No availability of slots are found. Please add on Appointment Booking Settings."
|
||||
msgstr "Inga lediga tider hittades. Lägg till detta i Tid Bokning Inställningar."
|
||||
msgstr "Inga lediga tider hittades. Lägg till detta i Tidsbokning Inställningar."
|
||||
|
||||
#: erpnext/stock/doctype/stock_reservation_entry/stock_reservation_entry.py:1367
|
||||
msgid "No available quantity to reserve for item {0} in warehouse {1}"
|
||||
@@ -32905,7 +32905,7 @@ msgstr "Inga utestående fakturor hittades"
|
||||
|
||||
#: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:353
|
||||
msgid "No outstanding invoices require exchange rate revaluation"
|
||||
msgstr "Inga utestående fakturor erfordrar växelkurs omvärdering"
|
||||
msgstr "Inga utestående fakturor kräver växelkurs omvärdering"
|
||||
|
||||
#: erpnext/accounts/doctype/payment_entry/payment_entry.py:2454
|
||||
msgid "No outstanding {0} found for the {1} {2} which qualify the filters you have specified."
|
||||
@@ -33021,7 +33021,7 @@ msgstr "Inga verifikat hittades för denna transaktion"
|
||||
|
||||
#: erpnext/accounts/doctype/sales_invoice/sales_invoice.py:2637
|
||||
msgid "No {0} found for Inter Company Transactions."
|
||||
msgstr "{0} hittades inte för Inter Bolag Transaktioner."
|
||||
msgstr "Ingen {0} hittades för Inter Bolag Transaktioner."
|
||||
|
||||
#. Label of the no_of_employees (Select) field in DocType 'Prospect'
|
||||
#: erpnext/crm/doctype/prospect/prospect.json
|
||||
@@ -35007,7 +35007,7 @@ msgstr "PDF Tabeller"
|
||||
|
||||
#: erpnext/accounts/doctype/bank_statement_import_log/bank_statement_import_log.py:930
|
||||
msgid "PDF statement support requires the 'pdfplumber' library to be installed."
|
||||
msgstr "Stöd för PDF kontoutdrag erfordrar att bibliotek \"pdfplumber\" är installerad."
|
||||
msgstr "Stöd för PDF kontoutdrag kräver att bibliotek \"pdfplumber\" är installerad."
|
||||
|
||||
#. Label of the pin (Data) field in DocType 'Warehouse'
|
||||
#: erpnext/stock/doctype/warehouse/warehouse.json
|
||||
@@ -35418,7 +35418,7 @@ msgstr "Sidbrytning efter varje SoA"
|
||||
|
||||
#: banking/src/components/features/BankStatementImporter/PDF/PDFTableEditor.tsx:302
|
||||
msgid "Page preview"
|
||||
msgstr "Förhandsvisa Sida"
|
||||
msgstr "Förhandsgranska Sida"
|
||||
|
||||
#. Option for the 'Status' (Select) field in DocType 'Payment Request'
|
||||
#. Option for the 'Status' (Select) field in DocType 'POS Invoice'
|
||||
@@ -37911,7 +37911,7 @@ msgstr "Lägg till Tillfällig Öppning Konto i Kontoplan"
|
||||
|
||||
#: erpnext/crm/doctype/appointment/appointment.py:95
|
||||
msgid "Please add a valid Holiday List on Appointment Booking Settings."
|
||||
msgstr "Lägg till giltig Helgdag Lista i Tid Bokning Inställningar."
|
||||
msgstr "Lägg till giltig Helgdag Lista i Tidsbokning Inställningar."
|
||||
|
||||
#: erpnext/accounts/doctype/bank_transaction_rule/bank_transaction_rule.py:119
|
||||
msgid "Please add an account for the Bank Entry rule."
|
||||
@@ -38073,11 +38073,11 @@ msgstr "Skapa inte mer än 500 Artiklar åt gång"
|
||||
|
||||
#: erpnext/accounts/doctype/budget/budget.py:182
|
||||
msgid "Please enable Applicable on Booking Actual Expenses"
|
||||
msgstr "Aktivera Tillämplig vid Bokföring av Faktiska Kostnader"
|
||||
msgstr "Aktivera Tillämpligt vid Bokföring av Faktiska Kostnader"
|
||||
|
||||
#: erpnext/accounts/doctype/budget/budget.py:178
|
||||
msgid "Please enable Applicable on Purchase Order and Applicable on Booking Actual Expenses"
|
||||
msgstr "Aktivera Tillämplig vid Inköp Order och Tillämplig vid Bokföring av Faktiska Kostnader"
|
||||
msgstr "Aktivera Tillämpligt vid Inköp Order och Tillämpligt vid Bokföring av Faktiska Kostnader"
|
||||
|
||||
#: erpnext/stock/doctype/pick_list/pick_list.py:323
|
||||
msgid "Please enable Use Old Serial / Batch Fields to make_bundle"
|
||||
@@ -38296,7 +38296,7 @@ msgstr "Fyll i Försäljning Order Tabell"
|
||||
|
||||
#: erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py:57
|
||||
msgid "Please fill up the Availability of Slots table to enable Appointment Scheduling."
|
||||
msgstr "Fyll i tabell ”Lediga Tider” för att aktivera Tid Bokning Schemaläggning."
|
||||
msgstr "Fyll i tabell ”Lediga Tider” för att aktivera Tidsbokning Schemaläggning."
|
||||
|
||||
#: erpnext/stock/doctype/shipment/shipment.js:277
|
||||
msgid "Please first set Full Name, Email and Phone for the user"
|
||||
@@ -38540,7 +38540,7 @@ msgstr "Välj Försäljning Följesedel"
|
||||
|
||||
#: erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py:81
|
||||
msgid "Please select a Holiday List to enable Appointment Scheduling."
|
||||
msgstr "Välj Helgdag Lista för att aktivera Tid Bokning Schemaläggning."
|
||||
msgstr "Välj Helgdag Lista för att aktivera Tidsbokning Schemaläggning."
|
||||
|
||||
#: erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py:153
|
||||
msgid "Please select a Subcontracting Purchase Order."
|
||||
@@ -38847,7 +38847,7 @@ msgstr "Ange Org.Nr. for Kund '%s'"
|
||||
|
||||
#: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:364
|
||||
msgid "Please set Unrealized Exchange Gain/Loss Account in Company {0}"
|
||||
msgstr "Ange Orealiserat Växelkurs Resultat Konto i Bolag {0}"
|
||||
msgstr "Ange Orealiserat Valutaväxling Resultat Konto i Bolag {0}"
|
||||
|
||||
#: erpnext/regional/report/vat_audit_report/vat_audit_report.py:56
|
||||
msgid "Please set VAT Accounts in {0}"
|
||||
@@ -38920,7 +38920,7 @@ msgstr "Ange Standard Kassa eller Bank Konto i Betalning Sätt {}"
|
||||
|
||||
#: erpnext/accounts/utils.py:2522
|
||||
msgid "Please set default Exchange Gain/Loss Account in Company {}"
|
||||
msgstr "Ange Standard Växelkurs Resultat Konto för Bolag {}"
|
||||
msgstr "Ange Standard Valutaväxling Resultat Konto för Bolag {}"
|
||||
|
||||
#: erpnext/assets/doctype/asset_repair/asset_repair.py:389
|
||||
msgid "Please set default Expense Account in Company {0}"
|
||||
@@ -39017,7 +39017,7 @@ msgstr "Ange {0} i {1} eller i Artikel Standard Inställningar {2}"
|
||||
|
||||
#: erpnext/accounts/doctype/payment_entry/payment_entry.py:1160
|
||||
msgid "Please set {0} in Company {1} to account for Exchange Gain / Loss"
|
||||
msgstr "Ange {0} i Bolag {1} för att bokföra växelkurs resultat"
|
||||
msgstr "Ange {0} i Bolag {1} för att bokföra valutaväxling resultat"
|
||||
|
||||
#: erpnext/controllers/accounts_controller.py:618
|
||||
msgid "Please set {0} to {1}, the same account that was used in the original invoice {2}."
|
||||
@@ -39296,7 +39296,7 @@ msgstr "Registrering Datum kan inte vara i framtiden"
|
||||
#. 'Accounts Settings'
|
||||
#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json
|
||||
msgid "Posting Date inheritance for exchange gain / loss"
|
||||
msgstr "Bokföring Datum arv för växelkurs resultat"
|
||||
msgstr "Bokföring Datum arv för valutaväxling resultat"
|
||||
|
||||
#: erpnext/public/js/controllers/transaction.js:1139
|
||||
msgid "Posting Date will change to today's date as Edit Posting Date and Time is unchecked. Are you sure want to proceed?"
|
||||
@@ -39538,17 +39538,17 @@ msgstr "Förhindrar att system automatiskt använder pris från senaste inköp t
|
||||
#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.js:267
|
||||
#: erpnext/buying/doctype/request_for_quotation/request_for_quotation.json
|
||||
msgid "Preview Email"
|
||||
msgstr "Förhandsvisa E-post"
|
||||
msgstr "Förhandsgranska E-post"
|
||||
|
||||
#. Label of the download_materials_request_plan_section_section (Section Break)
|
||||
#. field in DocType 'Production Plan'
|
||||
#: erpnext/manufacturing/doctype/production_plan/production_plan.json
|
||||
msgid "Preview Required Materials"
|
||||
msgstr "Förhandsvisa Erfordrad Material"
|
||||
msgstr "Förhandsgranska Erfordrad Material"
|
||||
|
||||
#: banking/src/components/features/BankStatementImporter/CSV/StatementDetails.tsx:230
|
||||
msgid "Preview Transactions"
|
||||
msgstr "Förhandsvisa Transaktioner"
|
||||
msgstr "Förhandsgranska Transaktioner"
|
||||
|
||||
#: erpnext/accounts/report/balance_sheet/balance_sheet.py:191
|
||||
#: erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py:142
|
||||
@@ -40053,7 +40053,7 @@ msgstr "Skriv ut"
|
||||
#. Profile'
|
||||
#: erpnext/accounts/doctype/pos_profile/pos_profile.json
|
||||
msgid "Print Receipt on Order Complete"
|
||||
msgstr "Skriv ut när Order är klar"
|
||||
msgstr "Skriv ut kvitto när Order är klar"
|
||||
|
||||
#: erpnext/setup/install.py:108
|
||||
msgid "Print UOM after Quantity"
|
||||
@@ -44374,7 +44374,7 @@ msgstr "Tog bort {0} rader med noll dokument antal. Spara för att ändringarna
|
||||
|
||||
#: erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py:87
|
||||
msgid "Removing rows without exchange gain or loss"
|
||||
msgstr "Tar bort rader utan Växelkurs Resultat"
|
||||
msgstr "Tar bort rader utan Valutaväxling Resultat"
|
||||
|
||||
#. Description of the 'Allow Rename Attribute Value' (Check) field in DocType
|
||||
#. 'Item Variant Settings'
|
||||
@@ -45908,7 +45908,7 @@ msgstr "Roll att avisera vid Avskrivning Fel"
|
||||
#. 'Company'
|
||||
#: erpnext/setup/doctype/company/company.json
|
||||
msgid "Roles Allowed to Set and Edit Frozen Account Entries"
|
||||
msgstr "Roller som får Ange och Redigera Låsta Konto Poster"
|
||||
msgstr "Roller som har behörighet att skapa och redigera stängda konto poster"
|
||||
|
||||
#. Label of the root (Link) field in DocType 'Bisect Nodes'
|
||||
#: erpnext/accounts/doctype/bisect_nodes/bisect_nodes.json
|
||||
@@ -58970,7 +58970,7 @@ msgstr "Okvalificerad"
|
||||
#. 'Company'
|
||||
#: erpnext/setup/doctype/company/company.json
|
||||
msgid "Unrealized Exchange Gain/Loss Account"
|
||||
msgstr "Orealiserad Växelkurs Resultat Konto"
|
||||
msgstr "Orealiserad Valutaväxling Resultat Konto"
|
||||
|
||||
#. Label of the unrealized_profit_loss_account (Link) field in DocType
|
||||
#. 'Purchase Invoice'
|
||||
@@ -59007,12 +59007,12 @@ msgstr "Ångra Avstämning"
|
||||
#: erpnext/workspace_sidebar/invoicing.json
|
||||
#: erpnext/workspace_sidebar/payments.json
|
||||
msgid "Unreconcile Payment"
|
||||
msgstr "Ångrad Betalning Avstämning"
|
||||
msgstr "Ångra Betalning Avstämning"
|
||||
|
||||
#. Name of a DocType
|
||||
#: erpnext/accounts/doctype/unreconcile_payment_entries/unreconcile_payment_entries.json
|
||||
msgid "Unreconcile Payment Entries"
|
||||
msgstr "Ångrad Betalning Avstämning Post"
|
||||
msgstr "Ångra Betalning Avstämning Post"
|
||||
|
||||
#: erpnext/accounts/doctype/bank_transaction/bank_transaction.js:40
|
||||
msgid "Unreconcile Transaction"
|
||||
@@ -62887,11 +62887,11 @@ msgstr "{0} är erfodrad för konto {1}"
|
||||
|
||||
#: erpnext/public/js/controllers/taxes_and_totals.js:132
|
||||
msgid "{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}"
|
||||
msgstr "{0} är erfordrad. Kanske Växelkurs Post är inte skapad för {1} till {2}"
|
||||
msgstr "{0} är erfordrad. Kanske Valutaväxling Post är inte skapad för {1} till {2}"
|
||||
|
||||
#: erpnext/controllers/accounts_controller.py:3212
|
||||
msgid "{0} is mandatory. Maybe Currency Exchange record is not created for {1} to {2}."
|
||||
msgstr "{0} är erfordrad. Kanske Växelkurs Post är inte skapad för {1} till {2}."
|
||||
msgstr "{0} är erfordrad. Kanske Valutaväxling Post är inte skapad för {1} till {2}."
|
||||
|
||||
#: erpnext/stock/doctype/serial_and_batch_bundle/serial_and_batch_bundle.py:1929
|
||||
msgid "{0} is not a CSV file."
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-08-09 09:47+0000\n"
|
||||
"PO-Revision-Date: 2026-08-26 11:44\n"
|
||||
"PO-Revision-Date: 2026-08-09 11:02\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Thai\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-08-09 09:47+0000\n"
|
||||
"PO-Revision-Date: 2026-08-26 11:44\n"
|
||||
"PO-Revision-Date: 2026-08-09 11:02\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Turkish\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-08-09 09:47+0000\n"
|
||||
"PO-Revision-Date: 2026-08-26 11:44\n"
|
||||
"PO-Revision-Date: 2026-08-09 11:03\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Uzbek\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-08-09 09:47+0000\n"
|
||||
"PO-Revision-Date: 2026-08-26 11:44\n"
|
||||
"PO-Revision-Date: 2026-08-09 11:02\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Vietnamese\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
||||
@@ -3,7 +3,7 @@ msgstr ""
|
||||
"Project-Id-Version: frappe\n"
|
||||
"Report-Msgid-Bugs-To: hello@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-08-09 09:47+0000\n"
|
||||
"PO-Revision-Date: 2026-08-26 11:43\n"
|
||||
"PO-Revision-Date: 2026-08-11 23:05\n"
|
||||
"Last-Translator: hello@frappe.io\n"
|
||||
"Language-Team: Chinese Simplified\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -51016,7 +51016,7 @@ msgstr ""
|
||||
#. Settings'
|
||||
#: erpnext/accounts/doctype/accounts_settings/accounts_settings.json
|
||||
msgid "Show balances in Chart of Accounts"
|
||||
msgstr "在会计科目表中显示余额"
|
||||
msgstr ""
|
||||
|
||||
#. Label of the show_barcode_field (Check) field in DocType 'Stock Settings'
|
||||
#: erpnext/stock/doctype/stock_settings/stock_settings.json
|
||||
|
||||
19661
erpnext/locale/zh_TW.po
19661
erpnext/locale/zh_TW.po
File diff suppressed because it is too large
Load Diff
@@ -4,10 +4,6 @@
|
||||
frappe.ui.form.on("Blanket Order", {
|
||||
onload: function (frm) {
|
||||
frm.trigger("set_tc_name_filter");
|
||||
if (frm.is_new()) {
|
||||
let has_pricing = frm.doc.currency || frm.doc.selling_price_list || frm.doc.buying_price_list;
|
||||
blanket_order_pricing.apply(frm, null, { reset_party_values: !has_pricing });
|
||||
}
|
||||
},
|
||||
|
||||
setup: function (frm) {
|
||||
@@ -19,13 +15,10 @@ frappe.ui.form.on("Blanket Order", {
|
||||
|
||||
frm.add_fetch("customer", "customer_name", "customer_name");
|
||||
frm.add_fetch("supplier", "supplier_name", "supplier_name");
|
||||
frm.set_query("selling_price_list", () => ({ filters: { selling: 1 } }));
|
||||
frm.set_query("buying_price_list", () => ({ filters: { buying: 1 } }));
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
erpnext.hide_company(frm);
|
||||
blanket_order_pricing.update_labels(frm);
|
||||
if (frm.doc.customer && frm.doc.docstatus === 1 && frm.doc.to_date > frappe.datetime.get_today()) {
|
||||
frm.add_custom_button(
|
||||
__("Sales Order"),
|
||||
@@ -108,141 +101,5 @@ frappe.ui.form.on("Blanket Order", {
|
||||
|
||||
blanket_order_type: function (frm) {
|
||||
frm.trigger("set_tc_name_filter");
|
||||
return reset_party_pricing(frm);
|
||||
},
|
||||
|
||||
company: reset_party_pricing,
|
||||
|
||||
customer: reset_party_pricing,
|
||||
|
||||
supplier: reset_party_pricing,
|
||||
|
||||
currency: function (frm) {
|
||||
return blanket_order_pricing.apply(frm, null, { reset_conversion_rate: true });
|
||||
},
|
||||
|
||||
from_date: function (frm) {
|
||||
return blanket_order_pricing.apply(frm, null, {
|
||||
reset_conversion_rate: true,
|
||||
reset_plc_conversion_rate: true,
|
||||
});
|
||||
},
|
||||
|
||||
conversion_rate: async function (frm) {
|
||||
await blanket_order_pricing.update_base_rates(frm);
|
||||
return blanket_order_pricing.apply(frm);
|
||||
},
|
||||
|
||||
selling_price_list: reset_price_list_exchange_rate,
|
||||
|
||||
buying_price_list: reset_price_list_exchange_rate,
|
||||
|
||||
plc_conversion_rate: function (frm) {
|
||||
return blanket_order_pricing.apply(frm);
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Blanket Order Item", {
|
||||
item_code: apply_item_pricing,
|
||||
|
||||
qty: apply_item_pricing,
|
||||
|
||||
rate: function (frm, cdt, cdn) {
|
||||
return set_base_rate(frm, frappe.get_doc(cdt, cdn));
|
||||
},
|
||||
});
|
||||
|
||||
const blanket_order_pricing = {
|
||||
update_base_rates(frm) {
|
||||
return Promise.all((frm.doc.items || []).map((item) => set_base_rate(frm, item)));
|
||||
},
|
||||
|
||||
update_labels(frm) {
|
||||
let company_currency = this.get_company_currency(frm);
|
||||
let show_base_rate = Boolean(
|
||||
frm.doc.currency && company_currency && frm.doc.currency !== company_currency
|
||||
);
|
||||
|
||||
frm.set_currency_labels(["price_list_rate", "rate"], frm.doc.currency || company_currency, "items");
|
||||
frm.set_currency_labels(["base_price_list_rate", "base_rate"], company_currency, "items");
|
||||
frm.fields_dict.items.grid.set_column_disp("base_price_list_rate", show_base_rate);
|
||||
frm.fields_dict.items.grid.set_column_disp("base_rate", show_base_rate);
|
||||
frm.toggle_display("conversion_rate", show_base_rate);
|
||||
frm.toggle_display(
|
||||
"plc_conversion_rate",
|
||||
Boolean(frm.doc.price_list_currency && frm.doc.price_list_currency !== company_currency)
|
||||
);
|
||||
frm.set_df_property(
|
||||
"conversion_rate",
|
||||
"description",
|
||||
show_base_rate ? `1 ${frm.doc.currency} = [?] ${company_currency}` : ""
|
||||
);
|
||||
frm.refresh_fields();
|
||||
},
|
||||
|
||||
get_company_currency(frm) {
|
||||
return frm.doc.company ? erpnext.get_currency(frm.doc.company) : null;
|
||||
},
|
||||
|
||||
async apply(frm, item_name = null, options = {}) {
|
||||
if (!frm.doc.company || !frm.doc.blanket_order_type) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (frm.__applying_blanket_order_price_list) {
|
||||
frm.__pending_blanket_order_price_list = { item_name, options };
|
||||
return;
|
||||
}
|
||||
|
||||
frm.__applying_blanket_order_price_list = true;
|
||||
let pending;
|
||||
try {
|
||||
let response = await frappe.call({
|
||||
method: "erpnext.manufacturing.doctype.blanket_order.blanket_order.apply_price_list",
|
||||
args: {
|
||||
doc: frm.doc,
|
||||
item_name,
|
||||
reset_party_values: options.reset_party_values,
|
||||
reset_conversion_rate: options.reset_conversion_rate,
|
||||
reset_plc_conversion_rate: options.reset_plc_conversion_rate,
|
||||
},
|
||||
});
|
||||
if (response.message) {
|
||||
await frm.set_value(response.message.parent);
|
||||
for (const values of response.message.children) {
|
||||
let { name, ...fields } = values;
|
||||
let item = (frm.doc.items || []).find((row) => row.name === name);
|
||||
if (item) {
|
||||
await frappe.model.set_value(item.doctype, item.name, fields);
|
||||
}
|
||||
}
|
||||
this.update_labels(frm);
|
||||
}
|
||||
} finally {
|
||||
frm.__applying_blanket_order_price_list = false;
|
||||
pending = frm.__pending_blanket_order_price_list;
|
||||
frm.__pending_blanket_order_price_list = null;
|
||||
}
|
||||
if (pending) {
|
||||
return this.apply(frm, pending.item_name, pending.options);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
function reset_party_pricing(frm) {
|
||||
return blanket_order_pricing.apply(frm, null, { reset_party_values: true });
|
||||
}
|
||||
|
||||
function reset_price_list_exchange_rate(frm) {
|
||||
return blanket_order_pricing.apply(frm, null, { reset_plc_conversion_rate: true });
|
||||
}
|
||||
|
||||
function apply_item_pricing(frm, cdt, cdn) {
|
||||
return blanket_order_pricing.apply(frm, cdn);
|
||||
}
|
||||
|
||||
function set_base_rate(frm, item) {
|
||||
frappe.model.round_floats_in(item, ["rate"]);
|
||||
let base_rate = flt(flt(item.rate) * flt(frm.doc.conversion_rate), precision("base_rate", item));
|
||||
return frappe.model.set_value(item.doctype, item.name, "base_rate", base_rate);
|
||||
}
|
||||
|
||||
@@ -18,14 +18,6 @@
|
||||
"from_date",
|
||||
"to_date",
|
||||
"company",
|
||||
"currency_and_price_list",
|
||||
"currency",
|
||||
"conversion_rate",
|
||||
"column_break_price_list",
|
||||
"selling_price_list",
|
||||
"buying_price_list",
|
||||
"price_list_currency",
|
||||
"plc_conversion_rate",
|
||||
"section_break_12",
|
||||
"items",
|
||||
"amended_from",
|
||||
@@ -104,66 +96,6 @@
|
||||
"reqd": 1,
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval:doc.currency && doc.currency != erpnext.get_currency(doc.company)",
|
||||
"fieldname": "currency_and_price_list",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Currency and Price List"
|
||||
},
|
||||
{
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Currency",
|
||||
"options": "Currency",
|
||||
"print_hide": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_price_list",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"description": "Rate at which document currency is converted to company currency",
|
||||
"fieldname": "conversion_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Exchange Rate",
|
||||
"precision": "9",
|
||||
"print_hide": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.blanket_order_type == \"Selling\"",
|
||||
"fieldname": "selling_price_list",
|
||||
"fieldtype": "Link",
|
||||
"label": "Price List",
|
||||
"options": "Price List",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.blanket_order_type == \"Purchasing\"",
|
||||
"fieldname": "buying_price_list",
|
||||
"fieldtype": "Link",
|
||||
"label": "Price List",
|
||||
"options": "Price List",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "price_list_currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Price List Currency",
|
||||
"options": "Currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"description": "Rate at which Price List Currency is converted to Company Currency",
|
||||
"fieldname": "plc_conversion_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Price List Exchange Rate",
|
||||
"precision": "9",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_12",
|
||||
"fieldtype": "Section Break"
|
||||
@@ -215,7 +147,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-08-27 10:55:37.000000",
|
||||
"modified": "2024-12-05 15:44:21.520093",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Blanket Order",
|
||||
@@ -243,4 +175,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,6 @@ from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import flt, getdate
|
||||
|
||||
from erpnext import get_company_currency
|
||||
from erpnext.controllers.accounts_controller import validate_conversion_rate
|
||||
from erpnext.manufacturing.doctype.blanket_order import blanket_order_pricing
|
||||
from erpnext.stock.doctype.item.item import get_item_defaults
|
||||
|
||||
|
||||
@@ -28,10 +25,7 @@ class BlanketOrder(Document):
|
||||
|
||||
amended_from: DF.Link | None
|
||||
blanket_order_type: DF.Literal["", "Selling", "Purchasing"]
|
||||
buying_price_list: DF.Link | None
|
||||
company: DF.Link
|
||||
conversion_rate: DF.Float
|
||||
currency: DF.Link
|
||||
customer: DF.Link | None
|
||||
customer_name: DF.Data | None
|
||||
from_date: DF.Date
|
||||
@@ -39,9 +33,6 @@ class BlanketOrder(Document):
|
||||
naming_series: DF.Literal["MFG-BLR-.YYYY.-"]
|
||||
order_date: DF.Date | None
|
||||
order_no: DF.Data | None
|
||||
plc_conversion_rate: DF.Float
|
||||
price_list_currency: DF.Link | None
|
||||
selling_price_list: DF.Link | None
|
||||
supplier: DF.Link | None
|
||||
supplier_name: DF.Data | None
|
||||
tc_name: DF.Link | None
|
||||
@@ -49,42 +40,11 @@ class BlanketOrder(Document):
|
||||
to_date: DF.Date
|
||||
# end: auto-generated types
|
||||
|
||||
def before_validate(self):
|
||||
self.set_currency()
|
||||
self.set_conversion_rate()
|
||||
blanket_order_pricing.set_price_list(self)
|
||||
|
||||
def validate(self):
|
||||
self.validate_dates()
|
||||
self.validate_duplicate_items()
|
||||
self.validate_item_qty()
|
||||
self.set_party_item_code()
|
||||
self.set_base_rates()
|
||||
|
||||
def set_currency(self):
|
||||
if self.currency:
|
||||
return
|
||||
|
||||
config = blanket_order_pricing.get_order_type_config(self.blanket_order_type)
|
||||
party_type = config["party_type"]
|
||||
party = self.get(config["party_field"])
|
||||
party_currency = frappe.get_cached_value(party_type, party, "default_currency") if party else None
|
||||
self.currency = party_currency or get_company_currency(self.company)
|
||||
|
||||
def set_conversion_rate(self):
|
||||
company_currency = get_company_currency(self.company)
|
||||
if self.currency == company_currency:
|
||||
self.conversion_rate = 1.0
|
||||
elif not self.conversion_rate:
|
||||
self.conversion_rate = blanket_order_pricing.get_exchange_rate_to_company(self, self.currency)
|
||||
|
||||
validate_conversion_rate(
|
||||
self.currency,
|
||||
self.conversion_rate,
|
||||
self.meta.get_translated_label("conversion_rate"),
|
||||
self.company,
|
||||
)
|
||||
self.conversion_rate = flt(self.conversion_rate, self.precision("conversion_rate"))
|
||||
|
||||
def validate_dates(self):
|
||||
if getdate(self.from_date) > getdate(self.to_date):
|
||||
@@ -163,26 +123,6 @@ class BlanketOrder(Document):
|
||||
if flt(d.qty) < 0:
|
||||
frappe.throw(_("Row {0}: Quantity cannot be negative.").format(d.idx))
|
||||
|
||||
def set_base_rates(self):
|
||||
blanket_order_pricing.set_base_rates(self)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def apply_price_list(
|
||||
doc: str | dict,
|
||||
item_name: str | None = None,
|
||||
reset_party_values: bool = False,
|
||||
reset_plc_conversion_rate: bool = False,
|
||||
reset_conversion_rate: bool = False,
|
||||
):
|
||||
return blanket_order_pricing.apply_price_list(
|
||||
doc=doc,
|
||||
item_name=item_name,
|
||||
reset_party_values=reset_party_values,
|
||||
reset_plc_conversion_rate=reset_plc_conversion_rate,
|
||||
reset_conversion_rate=reset_conversion_rate,
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_order(source_name):
|
||||
@@ -196,12 +136,14 @@ def make_order(source_name):
|
||||
def update_item(source, target, source_parent):
|
||||
target_qty = source.get("qty") - source.get("ordered_qty")
|
||||
target.qty = target_qty if flt(target_qty) >= 0 else 0
|
||||
target.rate = source.get("rate")
|
||||
item = get_item_defaults(target.item_code, source_parent.company)
|
||||
if item:
|
||||
target.item_name = item.get("item_name")
|
||||
target.description = item.get("description")
|
||||
target.uom = item.get("stock_uom")
|
||||
target.against_blanket_order = 1
|
||||
target.blanket_order = source_name
|
||||
|
||||
target_doc = get_mapped_doc(
|
||||
"Blanket Order",
|
||||
@@ -210,10 +152,7 @@ def make_order(source_name):
|
||||
"Blanket Order": {"doctype": doctype, "postprocess": update_doc},
|
||||
"Blanket Order Item": {
|
||||
"doctype": doctype + " Item",
|
||||
"field_map": {
|
||||
"rate": "blanket_order_rate",
|
||||
"parent": "blanket_order",
|
||||
},
|
||||
"field_map": {"rate": "blanket_order_rate", "parent": "blanket_order"},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda item: not (flt(item.qty)) or (flt(item.qty) - flt(item.ordered_qty)) > 0,
|
||||
},
|
||||
|
||||
@@ -1,218 +0,0 @@
|
||||
# Copyright (c) 2026, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import cint, flt
|
||||
|
||||
from erpnext import get_company_currency
|
||||
from erpnext.accounts.party import get_default_price_list as get_party_default_price_list
|
||||
from erpnext.controllers.accounts_controller import validate_conversion_rate
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
from erpnext.stock.get_item_details import get_item_prices_for_stock_uom
|
||||
|
||||
_ORDER_TYPE_CONFIG = {
|
||||
"Selling": {
|
||||
"exchange_rate_type": "for_selling",
|
||||
"opposite_price_list_field": "buying_price_list",
|
||||
"party_field": "customer",
|
||||
"party_type": "Customer",
|
||||
"price_list_field": "selling_price_list",
|
||||
"price_list_type": "Selling",
|
||||
"settings_doctype": "Selling Settings",
|
||||
},
|
||||
"Purchasing": {
|
||||
"exchange_rate_type": "for_buying",
|
||||
"opposite_price_list_field": "selling_price_list",
|
||||
"party_field": "supplier",
|
||||
"party_type": "Supplier",
|
||||
"price_list_field": "buying_price_list",
|
||||
"price_list_type": "Buying",
|
||||
"settings_doctype": "Buying Settings",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def get_order_type_config(blanket_order_type):
|
||||
return _ORDER_TYPE_CONFIG[blanket_order_type]
|
||||
|
||||
|
||||
def get_exchange_rate_to_company(doc, currency):
|
||||
config = get_order_type_config(doc.blanket_order_type)
|
||||
return get_exchange_rate(
|
||||
currency,
|
||||
get_company_currency(doc.company),
|
||||
doc.from_date,
|
||||
config["exchange_rate_type"],
|
||||
)
|
||||
|
||||
|
||||
def set_price_list(doc, set_default=False, force_exchange_rate=False):
|
||||
config = get_order_type_config(doc.blanket_order_type)
|
||||
fieldname = config["price_list_field"]
|
||||
doc.set(config["opposite_price_list_field"], None)
|
||||
|
||||
if not doc.get(fieldname) and (doc.is_new() or set_default):
|
||||
doc.set(fieldname, get_default_price_list(doc))
|
||||
|
||||
price_list = doc.get(fieldname)
|
||||
if not price_list:
|
||||
clear_price_list(doc)
|
||||
return
|
||||
|
||||
price_list_type = config["price_list_type"].lower()
|
||||
price_list_details = frappe.get_cached_value(
|
||||
"Price List", price_list, ["currency", price_list_type, "enabled"], as_dict=True
|
||||
)
|
||||
if not price_list_details or not price_list_details.enabled:
|
||||
frappe.throw(_("Price List {0} is disabled or does not exist").format(frappe.bold(price_list)))
|
||||
if not price_list_details.get(price_list_type):
|
||||
frappe.throw(
|
||||
_("Price List {0} is not enabled for {1}").format(
|
||||
frappe.bold(price_list), frappe.bold(doc.blanket_order_type)
|
||||
)
|
||||
)
|
||||
|
||||
price_list_currency_changed = doc.price_list_currency != price_list_details.currency
|
||||
doc.price_list_currency = price_list_details.currency
|
||||
company_currency = get_company_currency(doc.company)
|
||||
if doc.price_list_currency == company_currency:
|
||||
doc.plc_conversion_rate = 1.0
|
||||
elif price_list_currency_changed or not doc.plc_conversion_rate or force_exchange_rate:
|
||||
doc.plc_conversion_rate = get_exchange_rate_to_company(doc, doc.price_list_currency)
|
||||
|
||||
validate_conversion_rate(
|
||||
doc.price_list_currency,
|
||||
doc.plc_conversion_rate,
|
||||
doc.meta.get_translated_label("plc_conversion_rate"),
|
||||
doc.company,
|
||||
)
|
||||
doc.plc_conversion_rate = flt(doc.plc_conversion_rate, doc.precision("plc_conversion_rate"))
|
||||
|
||||
|
||||
def clear_price_list(doc):
|
||||
doc.price_list_currency = None
|
||||
doc.plc_conversion_rate = 0
|
||||
for item in doc.items:
|
||||
item.price_list_rate = 0
|
||||
item.base_price_list_rate = 0
|
||||
|
||||
|
||||
def get_default_price_list(doc):
|
||||
config = get_order_type_config(doc.blanket_order_type)
|
||||
party_type = config["party_type"]
|
||||
party = doc.get(config["party_field"])
|
||||
if party:
|
||||
party_price_list = get_party_default_price_list(frappe.get_cached_doc(party_type, party))
|
||||
if party_price_list:
|
||||
return party_price_list
|
||||
|
||||
return frappe.db.get_single_value(config["settings_doctype"], config["price_list_field"])
|
||||
|
||||
|
||||
def get_price_list_rates(doc, item_name=None):
|
||||
price_list = doc.get(get_order_type_config(doc.blanket_order_type)["price_list_field"])
|
||||
items = [item for item in doc.items if item.item_code and (not item_name or item.name == item_name)]
|
||||
if not items:
|
||||
return []
|
||||
if not price_list:
|
||||
return [{"name": item.name, "price_list_rate": 0, "base_price_list_rate": 0} for item in items]
|
||||
|
||||
ctx = frappe._dict(
|
||||
{
|
||||
"price_list": price_list,
|
||||
"customer": doc.customer,
|
||||
"supplier": doc.supplier,
|
||||
"transaction_date": doc.from_date,
|
||||
}
|
||||
)
|
||||
item_prices = get_item_prices_for_stock_uom(ctx, list(dict.fromkeys(item.item_code for item in items)))
|
||||
rates = []
|
||||
for item in items:
|
||||
item_price = item_prices.get(item.item_code)
|
||||
qty = flt(item.qty) or 1
|
||||
packing_unit = flt(item_price.packing_unit) if item_price else 0
|
||||
price_list_rate = (
|
||||
item_price.price_list_rate
|
||||
if item_price and (not packing_unit or qty % packing_unit == 0)
|
||||
else None
|
||||
)
|
||||
rate_details = {"name": item.name, "price_list_rate": 0, "base_price_list_rate": 0}
|
||||
if price_list_rate is not None:
|
||||
rate = flt(price_list_rate) * flt(doc.plc_conversion_rate) / flt(doc.conversion_rate)
|
||||
price_list_rate, base_price_list_rate = get_rate_and_base_amount(
|
||||
doc, item, "price_list_rate", rate
|
||||
)
|
||||
rate, base_rate = get_rate_and_base_amount(doc, item, "rate", rate)
|
||||
rate_details.update(
|
||||
{
|
||||
"price_list_rate": price_list_rate,
|
||||
"base_price_list_rate": base_price_list_rate,
|
||||
"rate": rate,
|
||||
"base_rate": base_rate,
|
||||
}
|
||||
)
|
||||
rates.append(rate_details)
|
||||
|
||||
return rates
|
||||
|
||||
|
||||
def set_base_rates(doc):
|
||||
for item in doc.items:
|
||||
for fieldname in ("price_list_rate", "rate"):
|
||||
rate, base_rate = get_rate_and_base_amount(doc, item, fieldname, item.get(fieldname))
|
||||
item.set(fieldname, rate)
|
||||
item.set(f"base_{fieldname}", base_rate)
|
||||
|
||||
|
||||
def get_rate_and_base_amount(doc, item, fieldname, rate):
|
||||
rate = flt(rate, item.precision(fieldname))
|
||||
base_fieldname = f"base_{fieldname}"
|
||||
base_rate = flt(rate * flt(doc.conversion_rate), item.precision(base_fieldname))
|
||||
return rate, base_rate
|
||||
|
||||
|
||||
def apply_price_list(
|
||||
doc,
|
||||
item_name=None,
|
||||
reset_party_values=False,
|
||||
reset_plc_conversion_rate=False,
|
||||
reset_conversion_rate=False,
|
||||
):
|
||||
doc = frappe.get_doc(frappe.parse_json(doc))
|
||||
reset_party_values = cint(reset_party_values)
|
||||
reset_plc_conversion_rate = cint(reset_plc_conversion_rate)
|
||||
reset_conversion_rate = cint(reset_conversion_rate)
|
||||
if reset_party_values:
|
||||
doc.currency = None
|
||||
doc.conversion_rate = 0
|
||||
doc.selling_price_list = None
|
||||
doc.buying_price_list = None
|
||||
doc.price_list_currency = None
|
||||
doc.plc_conversion_rate = 0
|
||||
else:
|
||||
if reset_conversion_rate:
|
||||
doc.conversion_rate = 0
|
||||
if reset_plc_conversion_rate:
|
||||
doc.plc_conversion_rate = 0
|
||||
|
||||
doc.set_currency()
|
||||
doc.set_conversion_rate()
|
||||
set_price_list(
|
||||
doc,
|
||||
set_default=reset_party_values,
|
||||
force_exchange_rate=reset_party_values or reset_plc_conversion_rate,
|
||||
)
|
||||
|
||||
return {
|
||||
"parent": {
|
||||
"currency": doc.currency,
|
||||
"conversion_rate": doc.conversion_rate,
|
||||
"selling_price_list": doc.selling_price_list,
|
||||
"buying_price_list": doc.buying_price_list,
|
||||
"price_list_currency": doc.price_list_currency,
|
||||
"plc_conversion_rate": doc.plc_conversion_rate,
|
||||
},
|
||||
"children": get_price_list_rates(doc, item_name),
|
||||
}
|
||||
@@ -1,18 +1,13 @@
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
from unittest.mock import patch
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_months, flt, today
|
||||
from frappe.utils import add_months, today
|
||||
|
||||
from erpnext import get_company_currency
|
||||
from erpnext.controllers.queries import get_blanket_orders
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.get_item_details import get_blanket_order_details
|
||||
from erpnext.tests.utils import ERPNextTestSuite
|
||||
|
||||
from . import blanket_order_pricing
|
||||
from .blanket_order import apply_price_list, make_order
|
||||
from .blanket_order import make_order
|
||||
|
||||
|
||||
class TestBlanketOrder(ERPNextTestSuite):
|
||||
@@ -144,241 +139,21 @@ class TestBlanketOrder(ERPNextTestSuite):
|
||||
bo = make_blanket_order(blanket_order_type="Purchasing", supplier=supplier, item_code=item_code)
|
||||
self.assertEqual(bo.items[0].party_item_code, "SUPP-PART-1")
|
||||
|
||||
def test_multicurrency_blanket_order(self):
|
||||
company_currency = get_company_currency("_Test Company")
|
||||
transaction_currency = "USD" if company_currency != "USD" else "EUR"
|
||||
conversion_rate = 80
|
||||
rate = 5
|
||||
|
||||
for blanket_order_type, target_doctypes in (
|
||||
("Selling", ("Sales Order", "Quotation")),
|
||||
("Purchasing", ("Purchase Order",)),
|
||||
):
|
||||
blanket_order = make_blanket_order(
|
||||
blanket_order_type=blanket_order_type,
|
||||
currency=transaction_currency,
|
||||
conversion_rate=conversion_rate,
|
||||
rate=rate,
|
||||
)
|
||||
|
||||
self.assertEqual(blanket_order.currency, transaction_currency)
|
||||
self.assertEqual(blanket_order.conversion_rate, conversion_rate)
|
||||
self.assertEqual(blanket_order.items[0].base_rate, rate * conversion_rate)
|
||||
|
||||
for target_doctype in target_doctypes:
|
||||
with self.subTest(target_doctype=target_doctype):
|
||||
frappe.flags.args.doctype = target_doctype
|
||||
target = make_order(blanket_order.name)
|
||||
|
||||
self.assertEqual(target.currency, transaction_currency)
|
||||
self.assertEqual(target.conversion_rate, conversion_rate)
|
||||
self.assertEqual(target.items[0].rate, rate)
|
||||
self.assertEqual(target.items[0].base_rate, rate * conversion_rate)
|
||||
self.assertEqual(target.items[0].blanket_order_rate, rate)
|
||||
self.assertEqual(target.items[0].blanket_order, blanket_order.name)
|
||||
|
||||
def test_price_list_rates_and_mapping(self):
|
||||
company = "_Test Company"
|
||||
company_currency = get_company_currency(company)
|
||||
transaction_currency = "USD" if company_currency != "USD" else "EUR"
|
||||
conversion_rate = 80
|
||||
price_list_rate = 800
|
||||
|
||||
for blanket_order_type, price_list_field, target_doctypes in (
|
||||
("Selling", "selling_price_list", ("Sales Order", "Quotation")),
|
||||
("Purchasing", "buying_price_list", ("Purchase Order",)),
|
||||
):
|
||||
blanket_order, price_list = make_priced_blanket_order(
|
||||
blanket_order_type=blanket_order_type,
|
||||
company=company,
|
||||
currency=transaction_currency,
|
||||
conversion_rate=conversion_rate,
|
||||
price_list_rate=price_list_rate,
|
||||
qty=1000,
|
||||
)
|
||||
blanket_order.insert()
|
||||
blanket_order.submit()
|
||||
|
||||
expected_rate = price_list_rate / conversion_rate
|
||||
self.assertEqual(blanket_order.price_list_currency, company_currency)
|
||||
self.assertEqual(blanket_order.plc_conversion_rate, 1)
|
||||
self.assertEqual(blanket_order.items[0].price_list_rate, expected_rate)
|
||||
self.assertEqual(blanket_order.items[0].base_price_list_rate, price_list_rate)
|
||||
self.assertEqual(blanket_order.items[0].rate, expected_rate)
|
||||
self.assertEqual(blanket_order.items[0].base_rate, price_list_rate)
|
||||
|
||||
for target_doctype in target_doctypes:
|
||||
with self.subTest(target_doctype=target_doctype):
|
||||
frappe.flags.args.doctype = target_doctype
|
||||
target = make_order(blanket_order.name)
|
||||
|
||||
self.assertEqual(target.get(price_list_field), price_list)
|
||||
self.assertEqual(target.price_list_currency, company_currency)
|
||||
self.assertEqual(target.plc_conversion_rate, 1)
|
||||
self.assertEqual(target.items[0].price_list_rate, expected_rate)
|
||||
self.assertEqual(target.items[0].base_price_list_rate, price_list_rate)
|
||||
self.assertEqual(target.items[0].rate, expected_rate)
|
||||
self.assertEqual(target.items[0].blanket_order, blanket_order.name)
|
||||
|
||||
def test_applying_price_list_ignores_empty_item_rows(self):
|
||||
blanket_order = frappe.new_doc("Blanket Order")
|
||||
blanket_order.blanket_order_type = "Selling"
|
||||
blanket_order.company = "_Test Company"
|
||||
blanket_order.customer = "_Test Customer"
|
||||
blanket_order.from_date = today()
|
||||
blanket_order.append("items", {})
|
||||
|
||||
pricing = apply_price_list(blanket_order.as_dict())
|
||||
|
||||
self.assertEqual(pricing["children"], [])
|
||||
|
||||
def test_price_list_rate_is_fetched_on_item_selection(self):
|
||||
company = "_Test Company"
|
||||
company_currency = get_company_currency(company)
|
||||
price_list_rate = 800
|
||||
blanket_order, _price_list = make_priced_blanket_order(
|
||||
company=company,
|
||||
currency=company_currency,
|
||||
conversion_rate=1,
|
||||
price_list_rate=price_list_rate,
|
||||
qty=0,
|
||||
)
|
||||
item = blanket_order.items[0]
|
||||
|
||||
self.assertEqual(item.price_list_rate, price_list_rate)
|
||||
self.assertEqual(item.rate, price_list_rate)
|
||||
|
||||
def test_price_list_rates_fetch_item_prices_once(self):
|
||||
blanket_order = new_blanket_order("Selling")
|
||||
blanket_order.selling_price_list = "_Test Price List"
|
||||
for item_code in ("ITEM-1", "ITEM-2"):
|
||||
blanket_order.append("items", {"item_code": item_code, "qty": 1})
|
||||
|
||||
with patch.object(
|
||||
blanket_order_pricing, "get_item_prices_for_stock_uom", return_value={}
|
||||
) as get_item_prices:
|
||||
rates = blanket_order_pricing.get_price_list_rates(blanket_order)
|
||||
|
||||
self.assertEqual(len(rates), 2)
|
||||
get_item_prices.assert_called_once_with(
|
||||
frappe._dict(
|
||||
{
|
||||
"price_list": "_Test Price List",
|
||||
"customer": "_Test Customer",
|
||||
"supplier": None,
|
||||
"transaction_date": blanket_order.from_date,
|
||||
}
|
||||
),
|
||||
["ITEM-1", "ITEM-2"],
|
||||
)
|
||||
|
||||
def test_price_list_conversion_uses_currency_precision(self):
|
||||
company = "_Test Company"
|
||||
company_currency = get_company_currency(company)
|
||||
transaction_currency = "USD" if company_currency != "USD" else "EUR"
|
||||
conversion_rate = 95.47
|
||||
price_list_rate = 100
|
||||
blanket_order, _price_list = make_priced_blanket_order(
|
||||
company=company,
|
||||
currency=transaction_currency,
|
||||
conversion_rate=conversion_rate,
|
||||
price_list_rate=price_list_rate,
|
||||
)
|
||||
item = blanket_order.items[0]
|
||||
expected_rate = flt(price_list_rate / conversion_rate, item.precision("rate"))
|
||||
expected_base_rate = flt(expected_rate * conversion_rate, item.precision("base_rate"))
|
||||
|
||||
self.assertFalse(frappe.get_meta("Blanket Order Item").get_field("rate").precision)
|
||||
self.assertEqual(item.price_list_rate, expected_rate)
|
||||
self.assertEqual(item.base_price_list_rate, expected_base_rate)
|
||||
self.assertEqual(item.rate, expected_rate)
|
||||
self.assertEqual(item.base_rate, expected_base_rate)
|
||||
|
||||
blanket_order.insert()
|
||||
blanket_order.submit()
|
||||
|
||||
frappe.flags.args.doctype = "Sales Order"
|
||||
sales_order = make_order(blanket_order.name)
|
||||
sales_order.delivery_date = today()
|
||||
sales_order.insert()
|
||||
|
||||
self.assertEqual(sales_order.items[0].price_list_rate, item.price_list_rate)
|
||||
self.assertEqual(sales_order.items[0].base_price_list_rate, item.base_price_list_rate)
|
||||
self.assertEqual(sales_order.items[0].rate, item.rate)
|
||||
self.assertEqual(sales_order.items[0].base_rate, item.base_rate)
|
||||
|
||||
def test_applying_price_list_can_reset_conversion_rate(self):
|
||||
company_currency = get_company_currency("_Test Company")
|
||||
transaction_currency = "USD" if company_currency != "USD" else "EUR"
|
||||
blanket_order, _price_list = make_priced_blanket_order(
|
||||
currency=transaction_currency,
|
||||
conversion_rate=80,
|
||||
price_list_rate=100,
|
||||
)
|
||||
|
||||
with patch(
|
||||
"erpnext.manufacturing.doctype.blanket_order.blanket_order_pricing.get_exchange_rate",
|
||||
return_value=95.47,
|
||||
):
|
||||
pricing = apply_price_list(blanket_order.as_dict(), reset_conversion_rate=True)
|
||||
|
||||
self.assertEqual(pricing["parent"]["conversion_rate"], 95.47)
|
||||
expected_rate = flt(
|
||||
100 / pricing["parent"]["conversion_rate"],
|
||||
blanket_order.items[0].precision("rate"),
|
||||
)
|
||||
expected_base_rate = flt(
|
||||
expected_rate * pricing["parent"]["conversion_rate"],
|
||||
blanket_order.items[0].precision("base_rate"),
|
||||
)
|
||||
self.assertEqual(pricing["children"][0]["base_rate"], expected_base_rate)
|
||||
|
||||
def test_blanket_order_lookup_filters_currency(self):
|
||||
company_currency = get_company_currency("_Test Company")
|
||||
transaction_currency = "USD" if company_currency != "USD" else "EUR"
|
||||
blanket_order = make_blanket_order(
|
||||
blanket_order_type="Selling",
|
||||
currency=transaction_currency,
|
||||
conversion_rate=80,
|
||||
)
|
||||
|
||||
filters = {
|
||||
"company": blanket_order.company,
|
||||
"currency": transaction_currency,
|
||||
"blanket_order_type": "Selling",
|
||||
"item": blanket_order.items[0].item_code,
|
||||
}
|
||||
matching_orders = get_blanket_orders("Blanket Order", "", "name", 0, 20, filters)
|
||||
self.assertIn(blanket_order.name, [order[0] for order in matching_orders])
|
||||
|
||||
filters["currency"] = company_currency
|
||||
other_currency_orders = get_blanket_orders("Blanket Order", "", "name", 0, 20, filters)
|
||||
self.assertNotIn(blanket_order.name, [order[0] for order in other_currency_orders])
|
||||
|
||||
details = get_blanket_order_details(
|
||||
{
|
||||
"blanket_order": blanket_order.name,
|
||||
"company": blanket_order.company,
|
||||
"currency": company_currency,
|
||||
"customer": blanket_order.customer,
|
||||
"doctype": "Sales Order",
|
||||
"item_code": blanket_order.items[0].item_code,
|
||||
"transaction_date": today(),
|
||||
}
|
||||
)
|
||||
self.assertFalse(details)
|
||||
|
||||
|
||||
def make_blanket_order(**args):
|
||||
args = frappe._dict(args)
|
||||
bo = new_blanket_order(
|
||||
blanket_order_type=args.blanket_order_type,
|
||||
company=args.company or "_Test Company",
|
||||
currency=args.currency,
|
||||
conversion_rate=args.conversion_rate or 1,
|
||||
customer=args.customer,
|
||||
supplier=args.supplier,
|
||||
)
|
||||
bo = frappe.new_doc("Blanket Order")
|
||||
bo.blanket_order_type = args.blanket_order_type
|
||||
bo.company = args.company or "_Test Company"
|
||||
|
||||
if args.blanket_order_type == "Selling":
|
||||
bo.customer = args.customer or "_Test Customer"
|
||||
else:
|
||||
bo.supplier = args.supplier or "_Test Supplier"
|
||||
|
||||
bo.from_date = today()
|
||||
bo.to_date = add_months(bo.from_date, months=12)
|
||||
|
||||
bo.append(
|
||||
"items",
|
||||
{
|
||||
@@ -391,84 +166,3 @@ def make_blanket_order(**args):
|
||||
bo.insert()
|
||||
bo.submit()
|
||||
return bo
|
||||
|
||||
|
||||
def make_priced_blanket_order(
|
||||
blanket_order_type="Selling",
|
||||
company="_Test Company",
|
||||
currency=None,
|
||||
conversion_rate=1,
|
||||
price_list_rate=800,
|
||||
qty=1,
|
||||
):
|
||||
price_list = make_blanket_order_price_list(get_company_currency(company), price_list_rate)
|
||||
blanket_order = new_blanket_order(
|
||||
blanket_order_type=blanket_order_type,
|
||||
company=company,
|
||||
currency=currency,
|
||||
conversion_rate=conversion_rate,
|
||||
)
|
||||
config = blanket_order_pricing.get_order_type_config(blanket_order_type)
|
||||
blanket_order.set(config["price_list_field"], price_list)
|
||||
item = blanket_order.append("items", {"item_code": "_Test Item", "qty": qty, "rate": 0})
|
||||
pricing = apply_price_list(blanket_order.as_dict())
|
||||
blanket_order.update(pricing["parent"])
|
||||
item.update({key: value for key, value in pricing["children"][0].items() if key != "name"})
|
||||
|
||||
return blanket_order, price_list
|
||||
|
||||
|
||||
def new_blanket_order(
|
||||
blanket_order_type,
|
||||
company="_Test Company",
|
||||
currency=None,
|
||||
conversion_rate=1,
|
||||
customer=None,
|
||||
supplier=None,
|
||||
):
|
||||
blanket_order = frappe.new_doc("Blanket Order")
|
||||
blanket_order.blanket_order_type = blanket_order_type
|
||||
blanket_order.company = company
|
||||
blanket_order.currency = currency or get_company_currency(company)
|
||||
blanket_order.conversion_rate = conversion_rate
|
||||
blanket_order.from_date = today()
|
||||
blanket_order.to_date = add_months(blanket_order.from_date, months=12)
|
||||
|
||||
config = blanket_order_pricing.get_order_type_config(blanket_order_type)
|
||||
party = customer if config["party_field"] == "customer" else supplier
|
||||
blanket_order.set(config["party_field"], party or f"_Test {config['party_type']}")
|
||||
|
||||
return blanket_order
|
||||
|
||||
|
||||
def make_blanket_order_price_list(currency, price_list_rate):
|
||||
price_list = "_Test Blanket Order Price List"
|
||||
if not frappe.db.exists("Price List", price_list):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Price List",
|
||||
"price_list_name": price_list,
|
||||
"currency": currency,
|
||||
"selling": 1,
|
||||
"buying": 1,
|
||||
}
|
||||
).insert()
|
||||
else:
|
||||
frappe.db.set_value("Price List", price_list, {"currency": currency, "selling": 1, "buying": 1})
|
||||
|
||||
item_price = frappe.db.get_value(
|
||||
"Item Price", {"price_list": price_list, "item_code": "_Test Item"}, "name"
|
||||
)
|
||||
if item_price:
|
||||
frappe.db.set_value("Item Price", item_price, "price_list_rate", price_list_rate)
|
||||
else:
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Item Price",
|
||||
"price_list": price_list,
|
||||
"item_code": "_Test Item",
|
||||
"price_list_rate": price_list_rate,
|
||||
}
|
||||
).insert()
|
||||
|
||||
return price_list
|
||||
|
||||
@@ -10,10 +10,7 @@
|
||||
"party_item_code",
|
||||
"column_break_3",
|
||||
"qty",
|
||||
"price_list_rate",
|
||||
"base_price_list_rate",
|
||||
"rate",
|
||||
"base_rate",
|
||||
"ordered_qty",
|
||||
"section_break_7",
|
||||
"terms_and_conditions"
|
||||
@@ -44,37 +41,11 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Quantity"
|
||||
},
|
||||
{
|
||||
"fieldname": "price_list_rate",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Price List Rate",
|
||||
"options": "currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "base_price_list_rate",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Price List Rate (Company Currency)",
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Rate",
|
||||
"options": "currency",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "base_rate",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Rate (Company Currency)",
|
||||
"options": "Company:company:default_currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -103,7 +74,7 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-08-27 10:55:37.000000",
|
||||
"modified": "2024-03-27 13:06:40.083042",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Blanket Order Item",
|
||||
@@ -114,4 +85,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,6 @@ class BlanketOrderItem(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
base_price_list_rate: DF.Currency
|
||||
base_rate: DF.Currency
|
||||
item_code: DF.Link
|
||||
item_name: DF.Data | None
|
||||
ordered_qty: DF.Float
|
||||
@@ -23,7 +21,6 @@ class BlanketOrderItem(Document):
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
party_item_code: DF.Data | None
|
||||
price_list_rate: DF.Currency
|
||||
qty: DF.Float
|
||||
rate: DF.Currency
|
||||
terms_and_conditions: DF.Text | None
|
||||
|
||||
@@ -685,9 +685,7 @@ erpnext.bom.BomController = class BomController extends erpnext.TransactionContr
|
||||
get_bom_material_detail(doc, cdt, cdn, secondary_items);
|
||||
}
|
||||
|
||||
buying_price_list() {
|
||||
const doc = this.frm.doc;
|
||||
|
||||
buying_price_list(doc) {
|
||||
if (doc.rm_cost_as_per !== "Price List" && doc.buying_price_list) {
|
||||
this.frm.set_value("buying_price_list", "");
|
||||
return;
|
||||
@@ -698,8 +696,8 @@ erpnext.bom.BomController = class BomController extends erpnext.TransactionContr
|
||||
}
|
||||
}
|
||||
|
||||
plc_conversion_rate() {
|
||||
if (!this.in_apply_price_list && this.frm.doc.rm_cost_as_per === "Price List") {
|
||||
plc_conversion_rate(doc) {
|
||||
if (!this.in_apply_price_list && doc.rm_cost_as_per === "Price List") {
|
||||
this.apply_price_list(null, true);
|
||||
}
|
||||
}
|
||||
@@ -755,7 +753,6 @@ var get_bom_material_detail = function (doc, cdt, cdn, secondary_items) {
|
||||
conversion_factor: d.conversion_factor,
|
||||
sourced_by_supplier: d.sourced_by_supplier,
|
||||
do_not_explode: d.do_not_explode,
|
||||
source_warehouse: d.source_warehouse || doc.default_source_warehouse,
|
||||
fetch_rate: !secondary_items,
|
||||
},
|
||||
callback: function (r) {
|
||||
@@ -766,10 +763,6 @@ var get_bom_material_detail = function (doc, cdt, cdn, secondary_items) {
|
||||
doc = locals[doc.doctype][doc.name];
|
||||
erpnext.bom.calculate_rm_cost(doc);
|
||||
erpnext.bom.calculate_total(doc);
|
||||
|
||||
if (secondary_items && d.valuation_type === "Valuation Rate") {
|
||||
erpnext.bom.fetch_secondary_item_cost(doc, cdt, cdn);
|
||||
}
|
||||
},
|
||||
freeze: true,
|
||||
});
|
||||
@@ -783,10 +776,11 @@ cur_frm.cscript.qty = function (doc) {
|
||||
|
||||
cur_frm.cscript.rate = function (doc, cdt, cdn) {
|
||||
var d = locals[cdt][cdn];
|
||||
const is_secondary_item = cdt == "BOM Secondary Item";
|
||||
|
||||
if (d.bom_no) {
|
||||
frappe.msgprint(__("You cannot change the rate if BOM is mentioned against any Item."));
|
||||
get_bom_material_detail(doc, cdt, cdn, false);
|
||||
get_bom_material_detail(doc, cdt, cdn, is_secondary_item);
|
||||
} else {
|
||||
erpnext.bom.calculate_rm_cost(doc);
|
||||
erpnext.bom.calculate_total(doc);
|
||||
@@ -949,9 +943,6 @@ frappe.ui.form.on("BOM Item", {
|
||||
do_not_explode: function (frm, cdt, cdn) {
|
||||
get_bom_material_detail(frm.doc, cdt, cdn, false);
|
||||
},
|
||||
source_warehouse: function (frm, cdt, cdn) {
|
||||
get_bom_material_detail(frm.doc, cdt, cdn, false);
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("BOM Item", "qty", function (frm, cdt, cdn) {
|
||||
@@ -1024,48 +1015,11 @@ frappe.tour["BOM"] = [
|
||||
];
|
||||
|
||||
frappe.ui.form.on("BOM Secondary Item", {
|
||||
valuation_type(frm, cdt, cdn) {
|
||||
const row = locals[cdt][cdn];
|
||||
if (row.valuation_type !== "% of FG Cost") {
|
||||
frappe.model.set_value(cdt, cdn, "cost_allocation_per", 0);
|
||||
}
|
||||
if (row.valuation_type === "Valuation Rate") {
|
||||
erpnext.bom.fetch_secondary_item_cost(frm.doc, cdt, cdn);
|
||||
} else if (row.valuation_type !== "Manual") {
|
||||
frappe.model.set_value(cdt, cdn, { cost: 0, base_cost: 0 });
|
||||
}
|
||||
item_code(frm, cdt, cdn) {
|
||||
const { item_code } = locals[cdt][cdn];
|
||||
},
|
||||
});
|
||||
|
||||
erpnext.bom.fetch_secondary_item_cost = function (doc, cdt, cdn) {
|
||||
const row = locals[cdt][cdn];
|
||||
if (!row.item_code) return;
|
||||
|
||||
frappe.call({
|
||||
doc: doc,
|
||||
method: "get_bom_material_detail",
|
||||
args: {
|
||||
company: doc.company,
|
||||
item_code: row.item_code,
|
||||
uom: row.uom,
|
||||
stock_uom: row.stock_uom,
|
||||
conversion_factor: row.conversion_factor,
|
||||
warehouse: doc.default_target_warehouse,
|
||||
set_rate_based_on_warehouse: 1,
|
||||
force_valuation_rate: 1,
|
||||
fetch_rate: 1,
|
||||
bom_no: "",
|
||||
},
|
||||
callback(r) {
|
||||
const cost = flt(r.message.rate) * flt(row.stock_qty);
|
||||
frappe.model.set_value(cdt, cdn, {
|
||||
cost: cost,
|
||||
base_cost: cost * flt(doc.conversion_rate || 1),
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
function trigger_process_loss_qty_prompt(frm, cdt, cdn, item_code) {
|
||||
frappe.prompt(
|
||||
{
|
||||
|
||||
@@ -392,7 +392,7 @@
|
||||
{
|
||||
"fetch_from": "item.description",
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Text Editor",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Item Description",
|
||||
"read_only": 1
|
||||
},
|
||||
@@ -761,7 +761,7 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-08-23 15:20:11.032436",
|
||||
"modified": "2026-04-17 15:22:33.598938",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM",
|
||||
|
||||
@@ -302,7 +302,7 @@ class BOM(WebsiteGenerator):
|
||||
self.set_default_uom()
|
||||
self.validate_semi_finished_goods()
|
||||
self.validate_secondary_items()
|
||||
self.validate_secondary_items_cost()
|
||||
self.set_fg_cost_allocation()
|
||||
self.validate_total_cost_allocation()
|
||||
|
||||
if self.docstatus == 1:
|
||||
@@ -361,20 +361,8 @@ class BOM(WebsiteGenerator):
|
||||
)
|
||||
|
||||
def validate_secondary_items(self):
|
||||
seen_items = set()
|
||||
for item in self.secondary_items:
|
||||
# every consumer merges secondary rows by item and type, so duplicates cannot
|
||||
# keep their own quantities, percentages or valuation mode
|
||||
key = (item.item_code, item.secondary_item_type or "")
|
||||
if key in seen_items:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Item {1} is already added with the same Type in the Secondary Items table."
|
||||
).format(item.idx, get_link_to_form("Item", item.item_code))
|
||||
)
|
||||
seen_items.add(key)
|
||||
|
||||
if item.valuation_type != "Valuation Rate" and item.item_code == self.item:
|
||||
if not item.is_legacy and item.item_code == self.item:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Finished Good Item {1} cannot be added in the Secondary Items table."
|
||||
@@ -465,25 +453,13 @@ class BOM(WebsiteGenerator):
|
||||
|
||||
def set_fg_cost_allocation(self):
|
||||
total_secondary_items_per = 0
|
||||
own_cost = 0
|
||||
for item in self.secondary_items:
|
||||
if item.valuation_type in ("Valuation Rate", "Manual"):
|
||||
item.cost_allocation_per = 0
|
||||
own_cost += flt(item.cost)
|
||||
total_secondary_items_per += item.cost_allocation_per
|
||||
|
||||
if self.cost_allocation_per == 100 and total_secondary_items_per:
|
||||
self.cost_allocation_per -= total_secondary_items_per
|
||||
|
||||
self.cost_allocation = (self.raw_material_cost - own_cost) * (self.cost_allocation_per / 100)
|
||||
|
||||
def validate_secondary_items_cost(self):
|
||||
if flt(self.secondary_items_cost) > flt(self.raw_material_cost):
|
||||
frappe.throw(
|
||||
_("The cost of the secondary items cannot exceed the raw material cost of {0}.").format(
|
||||
frappe.bold(flt(self.raw_material_cost))
|
||||
)
|
||||
)
|
||||
self.cost_allocation = self.raw_material_cost * (self.cost_allocation_per / 100)
|
||||
|
||||
def validate_total_cost_allocation(self):
|
||||
total_cost_allocation_per = self.cost_allocation_per
|
||||
@@ -554,7 +530,6 @@ class BOM(WebsiteGenerator):
|
||||
"conversion_factor": item.conversion_factor,
|
||||
"sourced_by_supplier": item.sourced_by_supplier,
|
||||
"do_not_explode": item.do_not_explode,
|
||||
"source_warehouse": item.source_warehouse or self.default_source_warehouse,
|
||||
"fetch_rate": True,
|
||||
}
|
||||
)
|
||||
@@ -640,12 +615,7 @@ class BOM(WebsiteGenerator):
|
||||
if not self.rm_cost_as_per:
|
||||
self.rm_cost_as_per = "Valuation Rate"
|
||||
|
||||
if arg and arg.get("force_valuation_rate"):
|
||||
# Valuation Rate secondary items ignore the BOM's rm_cost_as_per method:
|
||||
# bin-average valuation like the raw materials, scoped to the default
|
||||
# target warehouse when set.
|
||||
rate = get_valuation_rate(arg)
|
||||
elif arg:
|
||||
if arg:
|
||||
# Customer Provided parts and Supplier sourced parts will have zero rate
|
||||
if not frappe.db.get_value("Item", arg["item_code"], "is_customer_provided_item") and not arg.get(
|
||||
"sourced_by_supplier"
|
||||
@@ -980,7 +950,6 @@ class BOM(WebsiteGenerator):
|
||||
self.calculate_op_cost(update_hour_rate)
|
||||
self.calculate_rm_cost(save=save_updates)
|
||||
self.calculate_secondary_items_costs(save=save_updates)
|
||||
self.set_fg_cost_allocation()
|
||||
if save_updates:
|
||||
# not via doc event, table is not regenerated and needs updation
|
||||
self.calculate_exploded_cost()
|
||||
@@ -1067,7 +1036,6 @@ class BOM(WebsiteGenerator):
|
||||
"conversion_factor": d.conversion_factor,
|
||||
"sourced_by_supplier": d.sourced_by_supplier,
|
||||
"is_phantom_item": d.is_phantom_item,
|
||||
"source_warehouse": d.source_warehouse or self.default_source_warehouse,
|
||||
},
|
||||
notify=False,
|
||||
)
|
||||
@@ -1090,54 +1058,24 @@ class BOM(WebsiteGenerator):
|
||||
self.base_raw_material_cost = base_total_rm_cost
|
||||
|
||||
def calculate_secondary_items_costs(self, save=False):
|
||||
"""Valuation Rate and Manual rows carry their own cost, deducted from the raw
|
||||
material cost; the % of FG Cost rows split the remainder by their percentage."""
|
||||
"""Fetch RM rate as per today's valuation rate and calculate totals"""
|
||||
total_sm_cost = 0
|
||||
base_total_sm_cost = 0
|
||||
precision = self.precision("raw_material_cost")
|
||||
allocation_basis = flt(self.raw_material_cost) - self._set_own_cost_secondary_items(precision, save)
|
||||
|
||||
for d in self.get("secondary_items"):
|
||||
if d.valuation_type not in ("Valuation Rate", "Manual"):
|
||||
d.cost = flt(allocation_basis * (d.cost_allocation_per / 100), precision)
|
||||
if not d.is_legacy:
|
||||
d.cost = flt(self.raw_material_cost * (d.cost_allocation_per / 100), precision)
|
||||
d.base_cost = flt(d.cost * self.conversion_rate, precision)
|
||||
|
||||
total_sm_cost += d.cost
|
||||
base_total_sm_cost += d.base_cost
|
||||
if save:
|
||||
d.db_update()
|
||||
|
||||
total_sm_cost += d.cost
|
||||
base_total_sm_cost += d.base_cost
|
||||
|
||||
self.secondary_items_cost = total_sm_cost
|
||||
self.base_secondary_items_cost = base_total_sm_cost
|
||||
|
||||
def _set_own_cost_secondary_items(self, precision, save) -> float:
|
||||
"""Cost of the rows valued on their own: fetched for Valuation Rate, kept for Manual."""
|
||||
total = 0.0
|
||||
for d in self.get("secondary_items"):
|
||||
if d.valuation_type == "Valuation Rate":
|
||||
rate = self.get_rm_rate(self._secondary_item_rate_args(d))
|
||||
d.cost = flt(flt(rate) * flt(d.stock_qty), precision)
|
||||
elif d.valuation_type == "Manual":
|
||||
d.cost = flt(d.cost, precision)
|
||||
else:
|
||||
continue
|
||||
|
||||
d.base_cost = flt(d.cost * self.conversion_rate, precision)
|
||||
total += d.cost
|
||||
if save:
|
||||
d.db_update()
|
||||
|
||||
return total
|
||||
|
||||
def _secondary_item_rate_args(self, d):
|
||||
return {
|
||||
"item_code": d.item_code,
|
||||
"company": self.company,
|
||||
"warehouse": self.default_target_warehouse,
|
||||
"set_rate_based_on_warehouse": 1,
|
||||
"force_valuation_rate": 1,
|
||||
}
|
||||
|
||||
def calculate_exploded_cost(self):
|
||||
"Set exploded row cost from it's parent BOM."
|
||||
rm_rate_map = self.get_rm_rate_map()
|
||||
@@ -1360,8 +1298,7 @@ class BOM(WebsiteGenerator):
|
||||
|
||||
def has_scrap_items(self):
|
||||
return any(
|
||||
d.get("secondary_item_type") == "Scrap" or d.get("valuation_type") == "Valuation Rate"
|
||||
for d in self.get("secondary_items")
|
||||
d.get("secondary_item_type") == "Scrap" or d.get("is_legacy") for d in self.get("secondary_items")
|
||||
)
|
||||
|
||||
|
||||
@@ -1428,12 +1365,8 @@ def get_valuation_rate(data):
|
||||
.where((bin_table.item_code == item_code) & (wh_table.company == company))
|
||||
)
|
||||
|
||||
warehouse = data.get("source_warehouse")
|
||||
if data.get("set_rate_based_on_warehouse") and data.get("warehouse"):
|
||||
warehouse = data.get("warehouse")
|
||||
|
||||
if warehouse:
|
||||
item_valuation = item_valuation.where(bin_table.warehouse == warehouse)
|
||||
item_valuation = item_valuation.where(bin_table.warehouse == data.get("warehouse"))
|
||||
|
||||
item_valuation = item_valuation.run(as_dict=True)[0]
|
||||
|
||||
@@ -1482,7 +1415,7 @@ def get_bom_items_as_dict(
|
||||
|
||||
if fetch_secondary_items:
|
||||
fetch_exploded = 0
|
||||
group_by_cond = "group by item_code, secondary_item_type"
|
||||
group_by_cond = "group by item_code"
|
||||
|
||||
# Did not use qty_consumed_per_unit in the query, as it leads to rounding loss
|
||||
query = """select
|
||||
@@ -1531,10 +1464,10 @@ def get_bom_items_as_dict(
|
||||
query, {"parent": bom, "qty": qty, "bom": bom, "company": company}, as_dict=True
|
||||
)
|
||||
elif fetch_secondary_items:
|
||||
query = query.format( # nosemgrep
|
||||
query = query.format(
|
||||
table="BOM Secondary Item",
|
||||
where_conditions=")",
|
||||
select_columns=", item.description, bom_item.cost_allocation_per, bom_item.process_loss_per, bom_item.secondary_item_type, bom_item.name, bom_item.valuation_type, bom_item.cost / nullif(bom_item.stock_qty, 0) as manual_rate",
|
||||
select_columns=", item.description, bom_item.cost_allocation_per, bom_item.process_loss_per, bom_item.secondary_item_type, bom_item.name, bom_item.is_legacy",
|
||||
is_stock_item=is_stock_item,
|
||||
qty_field="stock_qty",
|
||||
group_by_cond=group_by_cond,
|
||||
@@ -1557,9 +1490,6 @@ def get_bom_items_as_dict(
|
||||
|
||||
for item in items:
|
||||
key = item.item_code
|
||||
if fetch_secondary_items:
|
||||
key = (item.item_code, item.secondary_item_type or "")
|
||||
|
||||
if item.operation_row_id:
|
||||
key = (item.item_code, item.operation_row_id)
|
||||
|
||||
|
||||
@@ -435,7 +435,7 @@ class TestBOM(ERPNextTestSuite):
|
||||
fg_item_non_whole, fg_item_whole, bom_item = create_process_loss_bom_items()
|
||||
|
||||
bom_doc = create_bom_with_process_loss_item(
|
||||
fg_item_non_whole, bom_item, scrap_qty=2, process_loss_percentage=110
|
||||
fg_item_non_whole, bom_item, scrap_qty=2, scrap_rate=0, process_loss_percentage=110
|
||||
)
|
||||
# PL can't be > 100
|
||||
self.assertRaises(frappe.ValidationError, bom_doc.submit)
|
||||
@@ -462,224 +462,12 @@ class TestBOM(ERPNextTestSuite):
|
||||
"secondary_item_type": "Additional Finished Good",
|
||||
"qty": 1,
|
||||
"cost_allocation_per": 10,
|
||||
"valuation_type": "% of FG Cost",
|
||||
},
|
||||
)
|
||||
|
||||
# FG item of the BOM cannot also be a secondary item
|
||||
self.assertRaises(frappe.ValidationError, bom_doc.save)
|
||||
|
||||
@timeout
|
||||
def test_duplicate_secondary_item_not_allowed(self):
|
||||
fg_item = make_item(properties={"is_stock_item": 1}).name
|
||||
rm_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 100}).name
|
||||
scrap_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 50}).name
|
||||
|
||||
bom_doc = frappe.new_doc("BOM")
|
||||
bom_doc.item = fg_item
|
||||
bom_doc.quantity = 1
|
||||
bom_doc.company = "_Test Company"
|
||||
bom_doc.currency = "INR"
|
||||
bom_doc.append("items", {"item_code": rm_item, "qty": 1, "rate": 100.0})
|
||||
bom_doc.append(
|
||||
"secondary_items",
|
||||
{
|
||||
"item_code": scrap_item,
|
||||
"secondary_item_type": "Scrap",
|
||||
"qty": 1,
|
||||
"valuation_type": "Valuation Rate",
|
||||
},
|
||||
)
|
||||
bom_doc.append(
|
||||
"secondary_items",
|
||||
{
|
||||
"item_code": scrap_item,
|
||||
"secondary_item_type": "Scrap",
|
||||
"qty": 1,
|
||||
"cost_allocation_per": 10,
|
||||
"valuation_type": "% of FG Cost",
|
||||
},
|
||||
)
|
||||
self.assertRaises(frappe.ValidationError, bom_doc.save)
|
||||
|
||||
# the same item with a different type is a distinct secondary output
|
||||
bom_doc.secondary_items[1].secondary_item_type = "By-Product"
|
||||
bom_doc.save()
|
||||
|
||||
@timeout
|
||||
def test_secondary_item_manual_cost(self):
|
||||
fg_item = make_item(properties={"is_stock_item": 1}).name
|
||||
rm_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 100}).name
|
||||
by_product = make_item(properties={"is_stock_item": 1}).name
|
||||
|
||||
bom_doc = frappe.new_doc("BOM")
|
||||
bom_doc.item = fg_item
|
||||
bom_doc.quantity = 1
|
||||
bom_doc.company = "_Test Company"
|
||||
bom_doc.currency = "INR"
|
||||
bom_doc.append("items", {"item_code": rm_item, "qty": 10, "rate": 100.0})
|
||||
bom_doc.append(
|
||||
"secondary_items",
|
||||
{
|
||||
"item_code": by_product,
|
||||
"secondary_item_type": "By-Product",
|
||||
"qty": 1,
|
||||
"valuation_type": "Manual",
|
||||
"cost": 150,
|
||||
},
|
||||
)
|
||||
bom_doc.save()
|
||||
|
||||
row = bom_doc.secondary_items[0]
|
||||
self.assertEqual(row.cost, 150)
|
||||
self.assertEqual(row.cost_allocation_per, 0)
|
||||
self.assertEqual(bom_doc.secondary_items_cost, 150)
|
||||
self.assertEqual(bom_doc.total_cost, 850)
|
||||
self.assertEqual(bom_doc.cost_allocation, 850)
|
||||
|
||||
# a manual cost above the raw material cost would turn the finished good negative
|
||||
bom_doc.secondary_items[0].cost = 1100
|
||||
self.assertRaises(frappe.ValidationError, bom_doc.save)
|
||||
|
||||
@timeout
|
||||
def test_secondary_item_valuation_rate_method(self):
|
||||
fg_item = make_item(properties={"is_stock_item": 1}).name
|
||||
rm_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 100}).name
|
||||
scrap_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 50}).name
|
||||
by_product = make_item(properties={"is_stock_item": 1}).name
|
||||
|
||||
bom_doc = frappe.new_doc("BOM")
|
||||
bom_doc.item = fg_item
|
||||
bom_doc.quantity = 1
|
||||
bom_doc.company = "_Test Company"
|
||||
bom_doc.currency = "INR"
|
||||
bom_doc.append("items", {"item_code": rm_item, "qty": 10, "rate": 100.0})
|
||||
bom_doc.append(
|
||||
"secondary_items",
|
||||
{
|
||||
"item_code": scrap_item,
|
||||
"secondary_item_type": "Scrap",
|
||||
"qty": 2,
|
||||
"valuation_type": "Valuation Rate",
|
||||
},
|
||||
)
|
||||
bom_doc.append(
|
||||
"secondary_items",
|
||||
{
|
||||
"item_code": by_product,
|
||||
"secondary_item_type": "By-Product",
|
||||
"qty": 1,
|
||||
"cost_allocation_per": 10,
|
||||
"valuation_type": "% of FG Cost",
|
||||
},
|
||||
)
|
||||
bom_doc.save()
|
||||
|
||||
scrap_row = bom_doc.secondary_items[0]
|
||||
self.assertEqual(scrap_row.cost, 100)
|
||||
self.assertEqual(scrap_row.cost_allocation_per, 0)
|
||||
|
||||
# the by-product's percentage applies to the cost net of the valuation rate rows
|
||||
self.assertEqual(bom_doc.raw_material_cost, 1000)
|
||||
self.assertEqual(bom_doc.secondary_items[1].cost, 90)
|
||||
self.assertEqual(bom_doc.cost_allocation_per, 90)
|
||||
self.assertEqual(bom_doc.cost_allocation, 810)
|
||||
self.assertEqual(bom_doc.secondary_items_cost, 190)
|
||||
self.assertEqual(bom_doc.total_cost, 810)
|
||||
|
||||
@timeout
|
||||
def test_rm_rate_scoped_to_source_warehouse(self):
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
fg_item = make_item(properties={"is_stock_item": 1}).name
|
||||
rm_item = make_item(properties={"is_stock_item": 1}).name
|
||||
|
||||
make_stock_entry(item_code=rm_item, target="_Test Warehouse - _TC", qty=10, basic_rate=100)
|
||||
make_stock_entry(item_code=rm_item, target="_Test Warehouse 1 - _TC", qty=10, basic_rate=50)
|
||||
|
||||
bom_doc = frappe.new_doc("BOM")
|
||||
bom_doc.item = fg_item
|
||||
bom_doc.quantity = 1
|
||||
bom_doc.company = "_Test Company"
|
||||
bom_doc.currency = "INR"
|
||||
bom_doc.append("items", {"item_code": rm_item, "qty": 1, "source_warehouse": "_Test Warehouse - _TC"})
|
||||
bom_doc.save()
|
||||
self.assertEqual(bom_doc.items[0].rate, 100)
|
||||
|
||||
bom_doc.items[0].source_warehouse = None
|
||||
bom_doc.save()
|
||||
self.assertEqual(bom_doc.items[0].rate, 75)
|
||||
|
||||
bom_doc.default_source_warehouse = "_Test Warehouse 1 - _TC"
|
||||
bom_doc.save()
|
||||
self.assertEqual(bom_doc.items[0].rate, 50)
|
||||
|
||||
@timeout
|
||||
def test_secondary_item_rate_scoped_to_target_warehouse(self):
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
fg_item = make_item(properties={"is_stock_item": 1}).name
|
||||
rm_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 100}).name
|
||||
scrap_item = make_item(properties={"is_stock_item": 1}).name
|
||||
|
||||
make_stock_entry(item_code=scrap_item, target="_Test Warehouse - _TC", qty=10, basic_rate=40)
|
||||
make_stock_entry(item_code=scrap_item, target="_Test Warehouse 1 - _TC", qty=10, basic_rate=20)
|
||||
|
||||
bom_doc = frappe.new_doc("BOM")
|
||||
bom_doc.item = fg_item
|
||||
bom_doc.quantity = 1
|
||||
bom_doc.company = "_Test Company"
|
||||
bom_doc.currency = "INR"
|
||||
bom_doc.default_target_warehouse = "_Test Warehouse - _TC"
|
||||
bom_doc.append("items", {"item_code": rm_item, "qty": 10, "rate": 100.0})
|
||||
bom_doc.append(
|
||||
"secondary_items",
|
||||
{
|
||||
"item_code": scrap_item,
|
||||
"secondary_item_type": "Scrap",
|
||||
"qty": 1,
|
||||
"valuation_type": "Valuation Rate",
|
||||
},
|
||||
)
|
||||
bom_doc.save()
|
||||
self.assertEqual(bom_doc.secondary_items[0].cost, 40)
|
||||
|
||||
bom_doc.default_target_warehouse = None
|
||||
bom_doc.save()
|
||||
self.assertEqual(bom_doc.secondary_items[0].cost, 30)
|
||||
|
||||
@timeout
|
||||
def test_secondary_item_valuation_rate_refreshed_on_update_cost(self):
|
||||
fg_item = make_item(properties={"is_stock_item": 1}).name
|
||||
rm_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 100}).name
|
||||
scrap_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 50}).name
|
||||
|
||||
bom_doc = frappe.new_doc("BOM")
|
||||
bom_doc.item = fg_item
|
||||
bom_doc.quantity = 1
|
||||
bom_doc.company = "_Test Company"
|
||||
bom_doc.currency = "INR"
|
||||
bom_doc.append("items", {"item_code": rm_item, "qty": 10, "rate": 100.0})
|
||||
bom_doc.append(
|
||||
"secondary_items",
|
||||
{
|
||||
"item_code": scrap_item,
|
||||
"secondary_item_type": "Scrap",
|
||||
"qty": 2,
|
||||
"valuation_type": "Valuation Rate",
|
||||
},
|
||||
)
|
||||
bom_doc.save()
|
||||
bom_doc.submit()
|
||||
|
||||
frappe.db.set_value("Item", scrap_item, "valuation_rate", 80)
|
||||
bom_doc.update_cost()
|
||||
bom_doc.reload()
|
||||
|
||||
self.assertEqual(bom_doc.secondary_items[0].cost, 160)
|
||||
self.assertEqual(bom_doc.total_cost, 840)
|
||||
self.assertEqual(bom_doc.cost_allocation, 840)
|
||||
|
||||
@timeout
|
||||
def test_bom_item_query(self):
|
||||
query = partial(
|
||||
@@ -1338,7 +1126,7 @@ def reset_item_valuation_rate(item_code, warehouse_list=None, qty=None, rate=Non
|
||||
|
||||
|
||||
def create_bom_with_process_loss_item(
|
||||
fg_item, bom_item, scrap_qty=0, fg_qty=2, process_loss_percentage=0, company=None
|
||||
fg_item, bom_item, scrap_qty=0, scrap_rate=0, fg_qty=2, process_loss_percentage=0, company=None
|
||||
):
|
||||
bom_doc = frappe.new_doc("BOM")
|
||||
bom_doc.item = fg_item.item_code
|
||||
@@ -1360,11 +1148,11 @@ def create_bom_with_process_loss_item(
|
||||
"secondary_items",
|
||||
{
|
||||
"item_code": fg_item.item_code,
|
||||
"secondary_item_type": "Scrap",
|
||||
"qty": scrap_qty,
|
||||
"stock_qty": scrap_qty,
|
||||
"uom": fg_item.stock_uom,
|
||||
"stock_uom": fg_item.stock_uom,
|
||||
"rate": scrap_rate,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -38,13 +38,15 @@
|
||||
{
|
||||
"secondary_items":[
|
||||
{
|
||||
"amount": 2000.0,
|
||||
"doctype": "BOM Secondary Item",
|
||||
"item_code": "_Test Item Home Desktop 100",
|
||||
"parentfield": "secondary_items",
|
||||
"stock_qty": 1.0,
|
||||
"rate": 2000.0,
|
||||
"stock_uom": "_Test UOM",
|
||||
"secondary_item_type": "Scrap",
|
||||
"valuation_type": "Valuation Rate"
|
||||
"is_legacy": 1
|
||||
}
|
||||
],
|
||||
"items": [
|
||||
|
||||
@@ -91,10 +91,14 @@ class BOMCreator(Document):
|
||||
|
||||
key = (row.item_code, row.fg_reference_id)
|
||||
if key in item_map:
|
||||
parent_item_code = next(
|
||||
item.item_code for item in self.items if item.name == row.fg_reference_id
|
||||
)
|
||||
|
||||
frappe.throw(
|
||||
_(
|
||||
"Item {0} added multiple times under the same parent item {1} at rows {2} and {3}"
|
||||
).format(bold(row.item_code), bold(row.fg_item), item_map[key], row.idx),
|
||||
).format(bold(row.item_code), bold(parent_item_code), item_map[key], row.idx),
|
||||
title=_("Duplicate Item Under Same Parent"),
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -6,10 +6,13 @@
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"secondary_item_type",
|
||||
"rate",
|
||||
"column_break_gres",
|
||||
"is_legacy",
|
||||
"section_break_sbnk",
|
||||
"item_code",
|
||||
"item_name",
|
||||
"secondary_item_type",
|
||||
"uom",
|
||||
"column_break_atlf",
|
||||
"qty",
|
||||
@@ -22,7 +25,6 @@
|
||||
"column_break_wsra",
|
||||
"image_nygv",
|
||||
"section_break_ielf",
|
||||
"valuation_type",
|
||||
"cost_allocation_per",
|
||||
"process_loss_per",
|
||||
"column_break_gtbl",
|
||||
@@ -32,12 +34,13 @@
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"depends_on": "eval:!doc.is_legacy",
|
||||
"fieldname": "secondary_item_type",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Type",
|
||||
"options": "\nCo-Product\nBy-Product\nScrap\nAdditional Finished Good",
|
||||
"reqd": 1
|
||||
"mandatory_depends_on": "eval:!doc.is_legacy",
|
||||
"options": "\nCo-Product\nBy-Product\nScrap\nAdditional Finished Good"
|
||||
},
|
||||
{
|
||||
"fieldname": "item_code",
|
||||
@@ -60,9 +63,10 @@
|
||||
"fieldname": "cost",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Cost",
|
||||
"no_copy": 1,
|
||||
"non_negative": 1,
|
||||
"options": "currency",
|
||||
"read_only_depends_on": "eval:doc.valuation_type != 'Manual'",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -99,6 +103,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.is_legacy",
|
||||
"fieldname": "section_break_ielf",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
@@ -138,7 +143,6 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.valuation_type == '% of FG Cost'",
|
||||
"fieldname": "cost_allocation_per",
|
||||
"fieldtype": "Percent",
|
||||
"label": "Cost Allocation %",
|
||||
@@ -171,20 +175,33 @@
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Base Cost (Company Currency)",
|
||||
"no_copy": 1,
|
||||
"non_negative": 1,
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "Valuation Rate",
|
||||
"description": "Valuation Rate and Manual value this item on its own and deduct that cost from the raw material cost, like the pre-v16 scrap items. % of FG Cost allocates a percentage of the remaining raw material cost.",
|
||||
"fieldname": "valuation_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Valuation Type",
|
||||
"options": "Valuation Rate\n% of FG Cost\nManual",
|
||||
"reqd": 1,
|
||||
"show_description_on_click": 1
|
||||
"fieldname": "column_break_gres",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "is_legacy",
|
||||
"fieldname": "is_legacy",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Legacy",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.is_legacy",
|
||||
"fieldname": "rate",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Rate",
|
||||
"no_copy": 1,
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
|
||||
@@ -20,6 +20,7 @@ class BOMSecondaryItem(Document):
|
||||
cost_allocation_per: DF.Percent
|
||||
description: DF.TextEditor | None
|
||||
image: DF.AttachImage | None
|
||||
is_legacy: DF.Check
|
||||
item_code: DF.Link
|
||||
item_name: DF.Data | None
|
||||
parent: DF.Data
|
||||
@@ -28,11 +29,11 @@ class BOMSecondaryItem(Document):
|
||||
process_loss_per: DF.Percent
|
||||
process_loss_qty: DF.Float
|
||||
qty: DF.Float
|
||||
secondary_item_type: DF.Literal["", "Co-Product", "By-Product", "Scrap", "Additional Finished Good"]
|
||||
rate: DF.Currency
|
||||
stock_qty: DF.Float
|
||||
stock_uom: DF.Link | None
|
||||
secondary_item_type: DF.Literal["", "Co-Product", "By-Product", "Scrap", "Additional Finished Good"]
|
||||
uom: DF.Link
|
||||
valuation_type: DF.Literal["Valuation Rate", "% of FG Cost", "Manual"]
|
||||
# end: auto-generated types
|
||||
|
||||
pass
|
||||
|
||||
@@ -246,16 +246,15 @@ class JobCard(Document):
|
||||
|
||||
wo_qty = wo_qty + (wo_qty * over_production_percentage / 100)
|
||||
|
||||
job_card = frappe.qb.DocType("Job Card")
|
||||
job_card_qty = (
|
||||
frappe.qb.from_(job_card)
|
||||
.select(Sum(job_card.for_quantity - IfNull(job_card.pending_qty, 0)))
|
||||
.where(
|
||||
(job_card.work_order == self.work_order)
|
||||
& (job_card.operation_id == self.operation_id)
|
||||
& (job_card.docstatus != 2)
|
||||
)
|
||||
.run()
|
||||
job_card_qty = frappe.get_all(
|
||||
"Job Card",
|
||||
fields=[{"SUM": "for_quantity"}],
|
||||
filters={
|
||||
"work_order": self.work_order,
|
||||
"operation_id": self.operation_id,
|
||||
"docstatus": ["!=", 2],
|
||||
},
|
||||
as_list=1,
|
||||
)
|
||||
|
||||
job_card_qty = flt(job_card_qty[0][0]) if job_card_qty else 0
|
||||
@@ -293,10 +292,10 @@ class JobCard(Document):
|
||||
fetch_exploded=0,
|
||||
fetch_secondary_items=1,
|
||||
)
|
||||
for values in items_dict.values():
|
||||
for item_code, values in items_dict.items():
|
||||
values = frappe._dict(values)
|
||||
secondary_item = {
|
||||
"item_code": values.item_code,
|
||||
"item_code": item_code,
|
||||
"stock_qty": values.qty,
|
||||
"item_name": values.item_name,
|
||||
"stock_uom": values.stock_uom,
|
||||
@@ -304,10 +303,11 @@ class JobCard(Document):
|
||||
"bom_secondary_item": values.name,
|
||||
}
|
||||
|
||||
secondary_item["stock_qty"] -= flt(
|
||||
secondary_item["stock_qty"] * (flt(values.process_loss_per) / 100),
|
||||
self.precision("for_quantity"),
|
||||
)
|
||||
if not values.is_legacy:
|
||||
secondary_item["stock_qty"] -= flt(
|
||||
secondary_item["stock_qty"] * (values.process_loss_per / 100),
|
||||
self.precision("for_quantity"),
|
||||
)
|
||||
|
||||
self.append("secondary_items", secondary_item)
|
||||
|
||||
@@ -1757,7 +1757,7 @@ class JobCard(Document):
|
||||
ste.stock_entry.pro_doc = frappe.get_doc("Work Order", self.work_order)
|
||||
ste.stock_entry.set_secondary_items_from_job_card()
|
||||
for row in ste.stock_entry.items:
|
||||
if (row.secondary_item_type or row.valuation_type) and not row.t_warehouse:
|
||||
if (row.secondary_item_type or row.is_legacy_scrap_item) and not row.t_warehouse:
|
||||
row.t_warehouse = self.target_warehouse
|
||||
|
||||
if auto_submit:
|
||||
|
||||
@@ -20,7 +20,7 @@ from erpnext.manufacturing.doctype.job_card.job_card import (
|
||||
make_stock_entry as make_stock_entry_from_jc,
|
||||
)
|
||||
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import WorkOrder, make_job_card, make_work_order
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import WorkOrder, make_work_order
|
||||
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
|
||||
from erpnext.patches.v16_0.set_stock_uom_in_job_card import execute as set_stock_uom_in_job_card
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
@@ -318,43 +318,6 @@ class TestJobCard(ERPNextTestSuite):
|
||||
# transfer was made for 2 fg qty in first transfer Stock Entry
|
||||
self.assertEqual(transfer_entry_2.fg_completed_qty, 0)
|
||||
|
||||
def test_material_request_stock_entry_uses_job_card_coverage(self):
|
||||
from erpnext.stock.doctype.material_request.material_request import make_stock_entry
|
||||
|
||||
self.transfer_material_against = "Job Card"
|
||||
self.source_warehouse = "Stores - _TC"
|
||||
job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name})
|
||||
mr = make_material_request(job_card.name)
|
||||
mr.schedule_date = today()
|
||||
for row in mr.items:
|
||||
row.qty = flt(row.qty) / 2
|
||||
row.stock_qty = flt(row.stock_qty) / 2
|
||||
mr.submit()
|
||||
|
||||
stock_entry = make_stock_entry(mr.name)
|
||||
self.assertEqual(stock_entry.fg_completed_qty, job_card.for_quantity / 2)
|
||||
|
||||
selected_row = mr.items[0]
|
||||
try:
|
||||
frappe.flags.selected_children = {"items": [selected_row.name]}
|
||||
selected_stock_entry = make_stock_entry(mr.name)
|
||||
finally:
|
||||
frappe.flags.selected_children = None
|
||||
|
||||
self.assertEqual(
|
||||
[row.job_card_item for row in selected_stock_entry.items], [selected_row.job_card_item]
|
||||
)
|
||||
self.assertEqual(selected_stock_entry.fg_completed_qty, 0)
|
||||
|
||||
for row in mr.items:
|
||||
transferred_qty = flt(row.stock_qty) / 2
|
||||
frappe.db.set_value("Job Card Item", row.job_card_item, "transferred_qty", transferred_qty)
|
||||
frappe.db.set_value(row.doctype, row.name, "ordered_qty", transferred_qty)
|
||||
mr.reload()
|
||||
|
||||
repeated_stock_entry = make_stock_entry(mr.name)
|
||||
self.assertEqual(repeated_stock_entry.fg_completed_qty, job_card.for_quantity / 4)
|
||||
|
||||
@ERPNextTestSuite.change_settings("Manufacturing Settings", {"job_card_excess_transfer": 1})
|
||||
def test_job_card_excess_material_transfer(self):
|
||||
"Test transferring more than required RM against Job Card."
|
||||
@@ -768,7 +731,6 @@ class TestJobCard(ERPNextTestSuite):
|
||||
self.assertEqual(ste.job_card, job_card_name)
|
||||
self.assertEqual(ste.from_bom, 1.0)
|
||||
self.assertEqual(ste.bom_no, work_order.bom_no)
|
||||
self.assertEqual(ste.fg_completed_qty, frappe.get_value("Job Card", job_card_name, "for_quantity"))
|
||||
|
||||
def test_job_card_material_transfer_via_pick_list(self):
|
||||
from erpnext.stock.doctype.material_request.material_request import create_pick_list
|
||||
@@ -1341,7 +1303,6 @@ class TestJobCard(ERPNextTestSuite):
|
||||
"qty": 1,
|
||||
"process_loss_per": 10,
|
||||
"cost_allocation_per": 5,
|
||||
"valuation_type": "% of FG Cost",
|
||||
"secondary_item_type": "Scrap",
|
||||
},
|
||||
)
|
||||
@@ -1861,16 +1822,16 @@ class TestJobCard(ERPNextTestSuite):
|
||||
job_card.save()
|
||||
|
||||
job_card.complete_job_card(
|
||||
qty=2,
|
||||
qty=3,
|
||||
for_quantity=5,
|
||||
pending_qty=3,
|
||||
pending_qty=2,
|
||||
process_loss_qty=0,
|
||||
end_time="2024-04-01 09:00:00",
|
||||
)
|
||||
|
||||
job_card.reload()
|
||||
self.assertEqual(flt(job_card.for_quantity), 5)
|
||||
self.assertEqual(flt(job_card.pending_qty), 3)
|
||||
self.assertEqual(flt(job_card.pending_qty), 2)
|
||||
self.assertEqual(flt(job_card.process_loss_qty), 0)
|
||||
|
||||
job_card.submit()
|
||||
@@ -1878,29 +1839,13 @@ class TestJobCard(ERPNextTestSuite):
|
||||
|
||||
manufacturing_entry = frappe.get_doc(job_card.make_stock_entry_for_semi_fg_item())
|
||||
finished_item = next(row for row in manufacturing_entry.items if row.is_finished_item)
|
||||
self.assertEqual(flt(finished_item.qty), 2)
|
||||
self.assertEqual(flt(finished_item.qty), 3)
|
||||
manufacturing_entry.submit()
|
||||
|
||||
job_card.reload()
|
||||
self.assertEqual(flt(job_card.manufactured_qty), 2)
|
||||
self.assertEqual(flt(job_card.manufactured_qty), 3)
|
||||
self.assertEqual(job_card.status, "Completed")
|
||||
|
||||
make_job_card(
|
||||
work_order.name,
|
||||
[
|
||||
{
|
||||
"name": work_order.operations[0].name,
|
||||
"operation": "Pending Qty Op A",
|
||||
"qty": 3,
|
||||
"pending_qty": 3,
|
||||
}
|
||||
],
|
||||
)
|
||||
follow_up_job_card = frappe.get_last_doc(
|
||||
"Job Card", {"work_order": work_order.name, "operation_id": work_order.operations[0].name}
|
||||
)
|
||||
self.assertEqual(flt(follow_up_job_card.for_quantity), 3)
|
||||
|
||||
def test_semi_fg_sequence_needs_previous_operations_manufactured(self):
|
||||
from erpnext.manufacturing.doctype.operation.test_operation import make_operation
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
@@ -2649,98 +2594,6 @@ class TestJobCard(ERPNextTestSuite):
|
||||
self.assertEqual(s.items[3].item_code, "_Test Item")
|
||||
self.assertEqual(s.items[3].transfer_qty, 2)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Stock Entry Detail",
|
||||
s.items[3].name,
|
||||
{"secondary_item_type": None, "valuation_type": "Valuation Rate"},
|
||||
)
|
||||
|
||||
stock_entry = frappe.get_doc({"doctype": "Stock Entry", "work_order": self.work_order.name})
|
||||
used_secondary_items = stock_entry.get_used_secondary_items()
|
||||
self.assertEqual(used_secondary_items[("_Test Item", "Scrap")], 2)
|
||||
|
||||
def test_secondary_items_from_multiple_boms_stay_separate(self):
|
||||
"""Rows linked to different BOM rows keep their own quantity and valuation mode."""
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
secondary_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 50}).name
|
||||
|
||||
bom_links = []
|
||||
for cost_allocation_per in (0, 10):
|
||||
fg_item = make_item(properties={"is_stock_item": 1}).name
|
||||
rm_item = make_item(properties={"is_stock_item": 1, "valuation_rate": 100}).name
|
||||
|
||||
bom_doc = frappe.new_doc("BOM")
|
||||
bom_doc.item = fg_item
|
||||
bom_doc.quantity = 1
|
||||
bom_doc.company = "_Test Company"
|
||||
bom_doc.currency = "INR"
|
||||
bom_doc.append("items", {"item_code": rm_item, "qty": 1, "rate": 100.0})
|
||||
bom_doc.append(
|
||||
"secondary_items",
|
||||
{
|
||||
"item_code": secondary_item,
|
||||
"secondary_item_type": "Scrap",
|
||||
"qty": 1,
|
||||
"cost_allocation_per": cost_allocation_per,
|
||||
"valuation_type": "% of FG Cost" if cost_allocation_per else "Valuation Rate",
|
||||
},
|
||||
)
|
||||
bom_doc.save()
|
||||
bom_doc.submit()
|
||||
bom_links.append(bom_doc.secondary_items[0].name)
|
||||
|
||||
for row in frappe.get_doc("BOM", self.work_order.bom_no).items:
|
||||
make_stock_entry(
|
||||
item_code=row.item_code,
|
||||
target="_Test Warehouse - _TC",
|
||||
qty=10,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name})
|
||||
job_card.append(
|
||||
"secondary_items",
|
||||
{
|
||||
"item_code": secondary_item,
|
||||
"stock_qty": 2,
|
||||
"secondary_item_type": "Scrap",
|
||||
"bom_secondary_item": bom_links[0],
|
||||
},
|
||||
)
|
||||
job_card.append(
|
||||
"secondary_items",
|
||||
{
|
||||
"item_code": secondary_item,
|
||||
"stock_qty": 3,
|
||||
"secondary_item_type": "Scrap",
|
||||
"bom_secondary_item": bom_links[1],
|
||||
},
|
||||
)
|
||||
job_card.append(
|
||||
"time_logs",
|
||||
{
|
||||
"from_time": "2009-01-01 12:06:25",
|
||||
"to_time": "2009-01-01 12:37:25",
|
||||
"completed_qty": job_card.for_quantity,
|
||||
},
|
||||
)
|
||||
job_card.save()
|
||||
job_card.submit()
|
||||
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||
make_stock_entry as make_stock_entry_for_wo,
|
||||
)
|
||||
|
||||
s = frappe.get_doc(make_stock_entry_for_wo(self.work_order.name, "Manufacture"))
|
||||
|
||||
rows = {d.bom_secondary_item: d for d in s.items if d.item_code == secondary_item}
|
||||
self.assertEqual(len(rows), 2)
|
||||
self.assertEqual(rows[bom_links[0]].qty, 2)
|
||||
self.assertEqual(rows[bom_links[0]].valuation_type, "Valuation Rate")
|
||||
self.assertEqual(rows[bom_links[1]].qty, 3)
|
||||
self.assertEqual(rows[bom_links[1]].valuation_type, "% of FG Cost")
|
||||
|
||||
@ERPNextTestSuite.change_settings(
|
||||
"Manufacturing Settings", {"overproduction_percentage_for_work_order": 100}
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user