mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-15 11:18:48 +00:00
Compare commits
189 Commits
v16.0.0-be
...
coderabbit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
741e6a7e52 | ||
|
|
31d55248e4 | ||
|
|
994dd425d4 | ||
|
|
97711b7e83 | ||
|
|
7012345968 | ||
|
|
7bfcdb13b1 | ||
|
|
d6bdbfe266 | ||
|
|
2db09b3840 | ||
|
|
17ff48ab92 | ||
|
|
c807a7be7b | ||
|
|
44919be5a6 | ||
|
|
4ad8e55d06 | ||
|
|
931f0663b1 | ||
|
|
2bdcec0a7e | ||
|
|
eb38accb33 | ||
|
|
c5b1af84eb | ||
|
|
01c14b5ce4 | ||
|
|
a7155c1fdb | ||
|
|
b75e7a1188 | ||
|
|
d6087e5d92 | ||
|
|
70521fb9bf | ||
|
|
b5ee193566 | ||
|
|
a0256bd798 | ||
|
|
6515bb04bf | ||
|
|
b96b7bd046 | ||
|
|
7c1a947cd8 | ||
|
|
ccbbc60585 | ||
|
|
47af1cec1b | ||
|
|
ec06f4a71b | ||
|
|
d9a377108c | ||
|
|
ed73bd6626 | ||
|
|
3f78d6afed | ||
|
|
4e578c4f83 | ||
|
|
404e68bdc2 | ||
|
|
4f33ee01cf | ||
|
|
c7e7e02b5b | ||
|
|
4df20a3122 | ||
|
|
29048c3364 | ||
|
|
09cdb943ec | ||
|
|
0373f7f33f | ||
|
|
95877e73f0 | ||
|
|
16f4e12854 | ||
|
|
9cc8a42074 | ||
|
|
28febc69e8 | ||
|
|
eee78766cd | ||
|
|
826c74eb71 | ||
|
|
6da10b9f97 | ||
|
|
cd540ab4cc | ||
|
|
b2e4e76b97 | ||
|
|
479e412a44 | ||
|
|
d330700f39 | ||
|
|
dc85babb4d | ||
|
|
17a6392407 | ||
|
|
58db596027 | ||
|
|
7bca3bbcd8 | ||
|
|
b12b40f373 | ||
|
|
72edd86561 | ||
|
|
29f2ecbd6f | ||
|
|
8053303378 | ||
|
|
1a920c035b | ||
|
|
f106d0e762 | ||
|
|
5daa625fe8 | ||
|
|
9f599ee52d | ||
|
|
d82464b2f9 | ||
|
|
88e94aa53a | ||
|
|
95ff1d48ab | ||
|
|
3c6369d396 | ||
|
|
bee0e5d8d4 | ||
|
|
1e60076ade | ||
|
|
4ad624be9c | ||
|
|
026487dce7 | ||
|
|
9b5d215a7a | ||
|
|
2612152456 | ||
|
|
75839f36ba | ||
|
|
e08805128b | ||
|
|
ba94d02cb4 | ||
|
|
c404e3b093 | ||
|
|
cd5b913750 | ||
|
|
dcdafc79ee | ||
|
|
91e285efd7 | ||
|
|
e9e498cbd0 | ||
|
|
b24c38f332 | ||
|
|
b4fbda4da3 | ||
|
|
21ec4ed911 | ||
|
|
7deb407206 | ||
|
|
8c35a6ecdd | ||
|
|
92ec633a5c | ||
|
|
bcf6deec9a | ||
|
|
35379294c3 | ||
|
|
1252fed642 | ||
|
|
699e9b4452 | ||
|
|
38b4536300 | ||
|
|
25458d6ba6 | ||
|
|
1966584804 | ||
|
|
b2ddef8340 | ||
|
|
150c764205 | ||
|
|
b855eb54b3 | ||
|
|
7e8d19b0c8 | ||
|
|
48783d136f | ||
|
|
8b8f569da2 | ||
|
|
d050cd221d | ||
|
|
5cfd7ec32a | ||
|
|
b808a51d8f | ||
|
|
1125f96316 | ||
|
|
725a4fcf2d | ||
|
|
32cf6148aa | ||
|
|
ba2411b4ee | ||
|
|
ce1312764f | ||
|
|
3a4c1a9f9a | ||
|
|
945390502e | ||
|
|
ae7db7ea5a | ||
|
|
8a7e5d0626 | ||
|
|
82b6326e0b | ||
|
|
e42b751dce | ||
|
|
71e46b3ef5 | ||
|
|
765f9a9bbf | ||
|
|
cdbe8b909b | ||
|
|
956e5b1b68 | ||
|
|
147a5ee953 | ||
|
|
a2fadd9347 | ||
|
|
da7f28a3c3 | ||
|
|
a7e43eddad | ||
|
|
0520ab3c66 | ||
|
|
9145bf5563 | ||
|
|
355aa52cb8 | ||
|
|
cf449d8dcb | ||
|
|
98e864bea0 | ||
|
|
aab7cd1ae6 | ||
|
|
31142b2f47 | ||
|
|
c5d92d7999 | ||
|
|
d8fc369e38 | ||
|
|
145d40dec8 | ||
|
|
8a5fd5fe89 | ||
|
|
f68515210b | ||
|
|
c2358c6b3f | ||
|
|
8ed9ee9213 | ||
|
|
95e6c72539 | ||
|
|
2b3bdfe387 | ||
|
|
5391ca2a55 | ||
|
|
0e7f75f5c0 | ||
|
|
d17120909c | ||
|
|
74a5325a95 | ||
|
|
0ca6e19a85 | ||
|
|
383c33b02a | ||
|
|
079218ffbf | ||
|
|
563c2998ca | ||
|
|
d52d98666f | ||
|
|
e830cca886 | ||
|
|
7b592d8737 | ||
|
|
88b262abc7 | ||
|
|
a8d3e9bacc | ||
|
|
264dcf8539 | ||
|
|
f1f68ead7d | ||
|
|
5a17dd8d6d | ||
|
|
c93dba2895 | ||
|
|
a436c6a503 | ||
|
|
8235a551f0 | ||
|
|
ca47ae6fd8 | ||
|
|
3d0b28a198 | ||
|
|
58e217be6a | ||
|
|
5e58e344b2 | ||
|
|
c2b8b97d7d | ||
|
|
fa8007f949 | ||
|
|
33135899ab | ||
|
|
5806bcbb17 | ||
|
|
56def01240 | ||
|
|
d01c4b68fe | ||
|
|
aeece36d93 | ||
|
|
49c866db74 | ||
|
|
e2a01773a5 | ||
|
|
903e97af5f | ||
|
|
7b1d860c33 | ||
|
|
5659538e67 | ||
|
|
3b7d7aed4c | ||
|
|
3404419a1f | ||
|
|
47a6d34224 | ||
|
|
9194e6350a | ||
|
|
5fd7d46986 | ||
|
|
02941afd6a | ||
|
|
4b612c64a8 | ||
|
|
57f9353d90 | ||
|
|
8f91919933 | ||
|
|
231479a6e2 | ||
|
|
e64b6db2eb | ||
|
|
7fddbb6dc4 | ||
|
|
5a3fcbedb5 | ||
|
|
e10007c646 | ||
|
|
800a44a65f | ||
|
|
cf1d892d60 |
5
.github/workflows/server-tests-mariadb.yml
vendored
5
.github/workflows/server-tests-mariadb.yml
vendored
@@ -128,10 +128,9 @@ jobs:
|
||||
FRAPPE_BRANCH: ${{ github.event.client_payload.sha || github.event.inputs.branch }}
|
||||
|
||||
- name: Run Tests
|
||||
run: 'cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --total-builds ${{ strategy.job-total }} --build-number ${{ matrix.container }}'
|
||||
run: 'cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --total-builds ${{ strategy.job-total }} --build-number ${{ matrix.container }} --with-coverage'
|
||||
env:
|
||||
TYPE: server
|
||||
CAPTURE_COVERAGE: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
|
||||
- name: Show bench output
|
||||
@@ -140,7 +139,6 @@ jobs:
|
||||
|
||||
- name: Upload coverage data
|
||||
uses: actions/upload-artifact@v4
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
name: coverage-${{ matrix.container }}
|
||||
path: /home/runner/frappe-bench/sites/coverage.xml
|
||||
@@ -149,7 +147,6 @@ jobs:
|
||||
name: Coverage Wrap Up
|
||||
needs: test
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -7,6 +7,7 @@ from frappe.utils import (
|
||||
cint,
|
||||
date_diff,
|
||||
flt,
|
||||
formatdate,
|
||||
get_first_day,
|
||||
get_last_day,
|
||||
get_link_to_form,
|
||||
@@ -318,7 +319,7 @@ def get_already_booked_amount(doc, item):
|
||||
def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
|
||||
enable_check = "enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense"
|
||||
|
||||
accounts_frozen_upto = frappe.get_single_value("Accounts Settings", "acc_frozen_upto")
|
||||
accounts_frozen_upto = frappe.db.get_value("Company", doc.company, "accounts_frozen_till_date")
|
||||
|
||||
def _book_deferred_revenue_or_expense(
|
||||
item,
|
||||
|
||||
@@ -93,8 +93,10 @@ class Account(NestedSet):
|
||||
super().on_update()
|
||||
|
||||
def onload(self):
|
||||
frozen_accounts_modifier = frappe.get_single_value("Accounts Settings", "frozen_accounts_modifier")
|
||||
if not frozen_accounts_modifier or frozen_accounts_modifier in frappe.get_roles():
|
||||
role_allowed_for_frozen_entries = frappe.db.get_value(
|
||||
"Company", self.company, "role_allowed_for_frozen_entries"
|
||||
)
|
||||
if not role_allowed_for_frozen_entries or role_allowed_for_frozen_entries in frappe.get_roles():
|
||||
self.set_onload("can_freeze_account", True)
|
||||
|
||||
def autoname(self):
|
||||
@@ -303,10 +305,10 @@ class Account(NestedSet):
|
||||
if not doc_before_save or doc_before_save.freeze_account == self.freeze_account:
|
||||
return
|
||||
|
||||
frozen_accounts_modifier = frappe.get_cached_value(
|
||||
"Accounts Settings", "Accounts Settings", "frozen_accounts_modifier"
|
||||
role_allowed_for_frozen_entries = frappe.get_cached_value(
|
||||
"Company", self.company, "role_allowed_for_frozen_entries"
|
||||
)
|
||||
if not frozen_accounts_modifier or frozen_accounts_modifier not in frappe.get_roles():
|
||||
if not role_allowed_for_frozen_entries or role_allowed_for_frozen_entries not in frappe.get_roles():
|
||||
throw(_("You are not authorized to set Frozen value"))
|
||||
|
||||
def validate_balance_must_be_debit_or_credit(self):
|
||||
|
||||
@@ -160,6 +160,14 @@ frappe.treeview_settings["Account"] = {
|
||||
.options,
|
||||
description: __("Optional. This setting will be used to filter in various transactions."),
|
||||
},
|
||||
{
|
||||
fieldtype: "Link",
|
||||
fieldname: "account_category",
|
||||
label: __("Account Category"),
|
||||
options: frappe.get_meta("Account").fields.filter((d) => d.fieldname == "account_category")[0]
|
||||
.options,
|
||||
description: __("Optional. Used with Financial Report Template"),
|
||||
},
|
||||
{
|
||||
fieldtype: "Float",
|
||||
fieldname: "tax_rate",
|
||||
|
||||
@@ -228,6 +228,7 @@ def get():
|
||||
},
|
||||
_("Impairment"): {"account_number": "5224", "account_category": "Operating Expenses"},
|
||||
_("Tax Expense"): {"account_number": "5225", "account_category": "Tax Expense"},
|
||||
"account_number": "5200",
|
||||
},
|
||||
"root_type": "Expense",
|
||||
"account_number": "5000",
|
||||
|
||||
@@ -73,13 +73,12 @@
|
||||
"calculate_depr_using_total_days",
|
||||
"column_break_gjcc",
|
||||
"book_asset_depreciation_entry_automatically",
|
||||
"role_to_notify_on_depreciation_failure",
|
||||
"closing_settings_tab",
|
||||
"period_closing_settings_section",
|
||||
"acc_frozen_upto",
|
||||
"ignore_account_closing_balance",
|
||||
"use_legacy_controller_for_pcv",
|
||||
"column_break_25",
|
||||
"frozen_accounts_modifier",
|
||||
"tab_break_dpet",
|
||||
"show_balance_in_coa",
|
||||
"banking_tab",
|
||||
@@ -102,21 +101,6 @@
|
||||
"use_legacy_budget_controller"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"description": "Accounting entries are frozen up to this date. Nobody can create or modify entries except users with the role specified below",
|
||||
"fieldname": "acc_frozen_upto",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Accounts Frozen Till Date"
|
||||
},
|
||||
{
|
||||
"description": "Users with this role are allowed to set frozen accounts and create / modify accounting entries against frozen accounts",
|
||||
"fieldname": "frozen_accounts_modifier",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Role Allowed to Set Frozen Accounts and Edit Frozen Entries",
|
||||
"options": "Role"
|
||||
},
|
||||
{
|
||||
"default": "Billing Address",
|
||||
"description": "Address used to determine Tax Category in transactions",
|
||||
@@ -658,6 +642,13 @@
|
||||
"fieldname": "use_legacy_controller_for_pcv",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Legacy Controller For Period Closing Voucher"
|
||||
},
|
||||
{
|
||||
"description": "Users with this role will be notified if the asset depreciation gets failed",
|
||||
"fieldname": "role_to_notify_on_depreciation_failure",
|
||||
"fieldtype": "Link",
|
||||
"label": "Role to Notify on Depreciation Failure",
|
||||
"options": "Role"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -666,7 +657,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-10-20 14:06:08.870427",
|
||||
"modified": "2025-12-03 20:42:13.238050",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -11,7 +11,6 @@ from frappe.model.document import Document
|
||||
from frappe.utils import cint
|
||||
|
||||
from erpnext.accounts.utils import sync_auto_reconcile_config
|
||||
from erpnext.stock.utils import check_pending_reposting
|
||||
|
||||
|
||||
class AccountsSettings(Document):
|
||||
@@ -23,7 +22,6 @@ class AccountsSettings(Document):
|
||||
if TYPE_CHECKING:
|
||||
from frappe.types import DF
|
||||
|
||||
acc_frozen_upto: DF.Date | None
|
||||
add_taxes_from_item_tax_template: DF.Check
|
||||
add_taxes_from_taxes_and_charges_template: DF.Check
|
||||
allow_multi_currency_invoices_against_single_party_account: DF.Check
|
||||
@@ -50,7 +48,6 @@ class AccountsSettings(Document):
|
||||
enable_party_matching: DF.Check
|
||||
exchange_gain_loss_posting_date: DF.Literal["Invoice", "Payment", "Reconciliation Date"]
|
||||
fetch_valuation_rate_for_internal_transaction: DF.Check
|
||||
frozen_accounts_modifier: DF.Link | None
|
||||
general_ledger_remarks_length: DF.Int
|
||||
ignore_account_closing_balance: DF.Check
|
||||
ignore_is_opening_check_for_reporting: DF.Check
|
||||
@@ -64,6 +61,7 @@ class AccountsSettings(Document):
|
||||
receivable_payable_remarks_length: DF.Int
|
||||
reconciliation_queue_size: DF.Int
|
||||
role_allowed_to_over_bill: DF.Link | None
|
||||
role_to_notify_on_depreciation_failure: DF.Link | None
|
||||
role_to_override_stop_action: DF.Link | None
|
||||
round_row_wise_tax: DF.Check
|
||||
show_balance_in_coa: DF.Check
|
||||
@@ -100,9 +98,6 @@ class AccountsSettings(Document):
|
||||
if old_doc.show_payment_schedule_in_print != self.show_payment_schedule_in_print:
|
||||
self.enable_payment_schedule_in_print()
|
||||
|
||||
if old_doc.acc_frozen_upto != self.acc_frozen_upto:
|
||||
self.validate_pending_reposts()
|
||||
|
||||
if clear_cache:
|
||||
frappe.clear_cache()
|
||||
|
||||
@@ -129,10 +124,6 @@ class AccountsSettings(Document):
|
||||
validate_fields_for_doctype=False,
|
||||
)
|
||||
|
||||
def validate_pending_reposts(self):
|
||||
if self.acc_frozen_upto:
|
||||
check_pending_reposting(self.acc_frozen_upto)
|
||||
|
||||
def validate_and_sync_auto_reconcile_config(self):
|
||||
if self.has_value_changed("auto_reconciliation_job_trigger"):
|
||||
if (
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
frappe.ui.form.on("Accounts Settings", {
|
||||
refresh: function (frm) {
|
||||
frm.set_df_property("acc_frozen_upto", "label", "Books Closed Through");
|
||||
frm.set_df_property(
|
||||
"frozen_accounts_modifier",
|
||||
"label",
|
||||
"Role Allowed to Close Books & Make Changes to Closed Periods"
|
||||
);
|
||||
frm.set_df_property("credit_controller", "label", "Credit Manager");
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Company", {
|
||||
refresh: function (frm) {
|
||||
frm.set_df_property("accounts_frozen_till_date", "label", "Books Closed Through");
|
||||
frm.set_df_property(
|
||||
"role_allowed_for_frozen_entries",
|
||||
"label",
|
||||
"Role Allowed to Close Books & Make Changes to Closed Periods"
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ frappe.ui.form.on("Currency Exchange Settings", {
|
||||
to: "{to_currency}",
|
||||
};
|
||||
add_param(frm, r.message, params, result);
|
||||
} else if (frm.doc.service_provider == "frankfurter.app") {
|
||||
} else if (frm.doc.service_provider == "frankfurter.dev") {
|
||||
let result = ["rates", "{to_currency}"];
|
||||
let params = {
|
||||
base: "{from_currency}",
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
"fieldname": "service_provider",
|
||||
"fieldtype": "Select",
|
||||
"label": "Service Provider",
|
||||
"options": "frankfurter.app\nexchangerate.host\nCustom",
|
||||
"options": "frankfurter.dev\nexchangerate.host\nCustom",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -104,7 +104,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:06:47.653110",
|
||||
"modified": "2025-11-25 13:03:41.896424",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Currency Exchange Settings",
|
||||
@@ -141,8 +141,9 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ class CurrencyExchangeSettings(Document):
|
||||
disabled: DF.Check
|
||||
req_params: DF.Table[CurrencyExchangeSettingsDetails]
|
||||
result_key: DF.Table[CurrencyExchangeSettingsResult]
|
||||
service_provider: DF.Literal["frankfurter.app", "exchangerate.host", "Custom"]
|
||||
service_provider: DF.Literal["frankfurter.dev", "exchangerate.host", "Custom"]
|
||||
url: DF.Data | None
|
||||
use_http: DF.Check
|
||||
# end: auto-generated types
|
||||
@@ -60,7 +60,7 @@ class CurrencyExchangeSettings(Document):
|
||||
self.append("req_params", {"key": "date", "value": "{transaction_date}"})
|
||||
self.append("req_params", {"key": "from", "value": "{from_currency}"})
|
||||
self.append("req_params", {"key": "to", "value": "{to_currency}"})
|
||||
elif self.service_provider == "frankfurter.app":
|
||||
elif self.service_provider == "frankfurter.dev":
|
||||
self.set("result_key", [])
|
||||
self.set("req_params", [])
|
||||
|
||||
@@ -105,11 +105,11 @@ class CurrencyExchangeSettings(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_api_endpoint(service_provider: str | None = None, use_http: bool = False):
|
||||
if service_provider and service_provider in ["exchangerate.host", "frankfurter.app"]:
|
||||
if service_provider and service_provider in ["exchangerate.host", "frankfurter.dev"]:
|
||||
if service_provider == "exchangerate.host":
|
||||
api = "api.exchangerate.host/convert"
|
||||
elif service_provider == "frankfurter.app":
|
||||
api = "api.frankfurter.app/{transaction_date}"
|
||||
elif service_provider == "frankfurter.dev":
|
||||
api = "api.frankfurter.dev/v1/{transaction_date}"
|
||||
|
||||
protocol = "https://"
|
||||
if use_http:
|
||||
|
||||
@@ -252,7 +252,7 @@ class ExchangeRateRevaluation(Document):
|
||||
company_currency = erpnext.get_company_currency(company)
|
||||
precision = get_field_precision(
|
||||
frappe.get_meta("Exchange Rate Revaluation Account").get_field("new_balance_in_base_currency"),
|
||||
company_currency,
|
||||
currency=company_currency,
|
||||
)
|
||||
|
||||
if account_details:
|
||||
|
||||
@@ -100,7 +100,7 @@ class GLEntry(Document):
|
||||
self.validate_account_details(adv_adj)
|
||||
self.validate_dimensions_for_pl_and_bs()
|
||||
validate_balance_type(self.account, adv_adj)
|
||||
validate_frozen_account(self.account, adv_adj)
|
||||
validate_frozen_account(self.company, self.account, adv_adj)
|
||||
|
||||
if (
|
||||
self.voucher_type == "Journal Entry"
|
||||
@@ -276,7 +276,7 @@ class GLEntry(Document):
|
||||
)
|
||||
|
||||
def validate_party(self):
|
||||
validate_party_frozen_disabled(self.party_type, self.party)
|
||||
validate_party_frozen_disabled(self.company, self.party_type, self.party)
|
||||
validate_account_party_type(self)
|
||||
|
||||
def validate_currency(self):
|
||||
@@ -419,16 +419,16 @@ def update_outstanding_amt(
|
||||
ref_doc.set_status(update=True)
|
||||
|
||||
|
||||
def validate_frozen_account(account, adv_adj=None):
|
||||
def validate_frozen_account(company, account, adv_adj=None):
|
||||
frozen_account = frappe.get_cached_value("Account", account, "freeze_account")
|
||||
if frozen_account == "Yes" and not adv_adj:
|
||||
frozen_accounts_modifier = frappe.get_cached_value(
|
||||
"Accounts Settings", None, "frozen_accounts_modifier"
|
||||
role_allowed_for_frozen_entries = frappe.get_cached_value(
|
||||
"Company", company, "role_allowed_for_frozen_entries"
|
||||
)
|
||||
|
||||
if not frozen_accounts_modifier:
|
||||
if not role_allowed_for_frozen_entries:
|
||||
frappe.throw(_("Account {0} is frozen").format(account))
|
||||
elif frozen_accounts_modifier not in frappe.get_roles():
|
||||
elif role_allowed_for_frozen_entries not in frappe.get_roles():
|
||||
frappe.throw(_("Not authorized to edit frozen Account {0}").format(account))
|
||||
|
||||
|
||||
@@ -442,7 +442,7 @@ def update_against_account(voucher_type, voucher_no):
|
||||
if not entries:
|
||||
return
|
||||
company_currency = erpnext.get_company_currency(entries[0].company)
|
||||
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency)
|
||||
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), currency=company_currency)
|
||||
|
||||
accounts_debited, accounts_credited = [], []
|
||||
for d in entries:
|
||||
|
||||
@@ -720,6 +720,8 @@ $.extend(erpnext.journal_entry, {
|
||||
}
|
||||
},
|
||||
});
|
||||
} else {
|
||||
erpnext.journal_entry.clear_fields(frm, dt, dn);
|
||||
}
|
||||
},
|
||||
set_amount_on_last_row: function (frm, dt, dn) {
|
||||
@@ -744,4 +746,13 @@ $.extend(erpnext.journal_entry, {
|
||||
}
|
||||
refresh_field("accounts");
|
||||
},
|
||||
clear_fields: function (frm, dt, dn) {
|
||||
let row = locals[dt][dn];
|
||||
|
||||
row.party_type = null;
|
||||
row.party = null;
|
||||
row.bank_account = null;
|
||||
|
||||
frm.refresh_field("accounts");
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1719,6 +1719,9 @@ def get_account_details_and_party_type(account, date, company, debit=None, credi
|
||||
"party_type": party_type,
|
||||
"account_type": account_details.account_type,
|
||||
"account_currency": account_details.account_currency or company_currency,
|
||||
"bank_account": (
|
||||
frappe.db.get_value("Bank Account", {"account": account, "company": company}) or None
|
||||
),
|
||||
# The date used to retreive the exchange rate here is the date passed in
|
||||
# as an argument to this function. It is assumed to be the date on which the balance is sought
|
||||
"exchange_rate": get_exchange_rate(
|
||||
|
||||
@@ -1874,7 +1874,7 @@ class PaymentEntry(AccountsController):
|
||||
else:
|
||||
self.total_taxes_and_charges += current_tax_amount
|
||||
|
||||
self.base_total_taxes_and_charges += tax.base_tax_amount
|
||||
self.base_total_taxes_and_charges += current_tax_amount
|
||||
|
||||
if self.get("taxes"):
|
||||
self.paid_amount_after_tax = self.get("taxes")[-1].base_total
|
||||
|
||||
@@ -159,7 +159,7 @@ class PaymentLedgerEntry(Document):
|
||||
def on_update(self):
|
||||
adv_adj = self.flags.adv_adj
|
||||
if not self.flags.from_repost:
|
||||
validate_frozen_account(self.account, adv_adj)
|
||||
validate_frozen_account(self.company, self.account, adv_adj)
|
||||
if not self.delinked:
|
||||
self.validate_account_details()
|
||||
self.validate_dimensions_for_pl_and_bs()
|
||||
|
||||
@@ -334,7 +334,9 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
},
|
||||
{
|
||||
fieldtype: "HTML",
|
||||
options: "<b> New Journal Entry will be posted for the difference amount </b>",
|
||||
options: __(
|
||||
"New Journal Entry will be posted for the difference amount. The Posting Date can be modified."
|
||||
).bold(),
|
||||
},
|
||||
],
|
||||
primary_action: () => {
|
||||
|
||||
@@ -765,6 +765,14 @@ class PaymentReconciliation(Document):
|
||||
|
||||
def reconcile_dr_cr_note(dr_cr_notes, company, active_dimensions=None):
|
||||
for inv in dr_cr_notes:
|
||||
if (
|
||||
abs(frappe.db.get_value(inv.voucher_type, inv.voucher_no, "outstanding_amount"))
|
||||
< inv.allocated_amount
|
||||
):
|
||||
frappe.throw(
|
||||
_("{0} has been modified after you pulled it. Please pull it again.").format(inv.voucher_type)
|
||||
)
|
||||
|
||||
voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note"
|
||||
|
||||
reconcile_dr_or_cr = (
|
||||
|
||||
@@ -20,6 +20,11 @@ from erpnext.accounts.party import get_due_date, get_party_account
|
||||
from erpnext.controllers.queries import item_query as _item_query
|
||||
from erpnext.controllers.sales_and_purchase_return import get_sales_invoice_item_from_consolidated_invoice
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
from erpnext.stock.stock_ledger import is_negative_stock_allowed
|
||||
|
||||
|
||||
class ProductBundleStockValidationError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class POSInvoice(SalesInvoice):
|
||||
@@ -395,32 +400,67 @@ class POSInvoice(SalesInvoice):
|
||||
|
||||
for d in self.get("items"):
|
||||
if not d.serial_and_batch_bundle:
|
||||
available_stock, is_stock_item, is_negative_stock_allowed = get_stock_availability(
|
||||
d.item_code, d.warehouse
|
||||
)
|
||||
if frappe.db.exists("Product Bundle", d.item_code):
|
||||
(
|
||||
availability,
|
||||
is_stock_item,
|
||||
is_negative_stock_allowed,
|
||||
) = get_product_bundle_stock_availability(d.item_code, d.warehouse, d.stock_qty)
|
||||
|
||||
else:
|
||||
availability, is_stock_item, is_negative_stock_allowed = get_stock_availability(
|
||||
d.item_code, d.warehouse
|
||||
)
|
||||
|
||||
if is_negative_stock_allowed:
|
||||
continue
|
||||
|
||||
item_code, warehouse, _qty = (
|
||||
frappe.bold(d.item_code),
|
||||
frappe.bold(d.warehouse),
|
||||
frappe.bold(d.qty),
|
||||
)
|
||||
if is_stock_item and flt(available_stock) <= 0:
|
||||
frappe.throw(
|
||||
_("Row #{}: Item Code: {} is not available under warehouse {}.").format(
|
||||
d.idx, item_code, warehouse
|
||||
),
|
||||
title=_("Item Unavailable"),
|
||||
)
|
||||
elif is_stock_item and flt(available_stock) < flt(d.stock_qty):
|
||||
frappe.throw(
|
||||
_("Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}.").format(
|
||||
d.idx, item_code, warehouse
|
||||
),
|
||||
title=_("Item Unavailable"),
|
||||
)
|
||||
if isinstance(availability, list):
|
||||
error_msgs = []
|
||||
for item in availability:
|
||||
if flt(item["available"]) < flt(item["required"]):
|
||||
error_msgs.append(
|
||||
_("<li>Packed Item {0}: Required {1}, Available {2}</li>").format(
|
||||
frappe.bold(item["item_code"]),
|
||||
frappe.bold(flt(item["required"], 2)),
|
||||
frappe.bold(flt(item["available"], 2)),
|
||||
)
|
||||
)
|
||||
|
||||
if error_msgs:
|
||||
frappe.throw(
|
||||
_(
|
||||
"<b>Row #{0}:</b> Bundle {1} in warehouse {2} has insufficient packed items:<br><div style='margin-top: 15px;'><ul style='line-height: 0.8;'>{3}</ul></div>"
|
||||
).format(
|
||||
d.idx,
|
||||
frappe.bold(d.item_code),
|
||||
frappe.bold(d.warehouse),
|
||||
"<br>".join(error_msgs),
|
||||
),
|
||||
title=_("Insufficient Stock for Product Bundle Items"),
|
||||
exc=ProductBundleStockValidationError,
|
||||
)
|
||||
|
||||
else:
|
||||
item_code, warehouse = frappe.bold(d.item_code), frappe.bold(d.warehouse)
|
||||
if is_stock_item and flt(availability) <= 0:
|
||||
frappe.throw(
|
||||
_("Row #{0}: Item {1} has no stock in warehouse {2}.").format(
|
||||
d.idx, item_code, warehouse
|
||||
),
|
||||
title=_("Item Out of Stock"),
|
||||
)
|
||||
elif is_stock_item and flt(availability) < flt(d.stock_qty):
|
||||
frappe.throw(
|
||||
_("Row #{0}: Item {1} in warehouse {2}: Available {3}, Needed {4}.").format(
|
||||
d.idx,
|
||||
item_code,
|
||||
warehouse,
|
||||
frappe.bold(flt(availability, 2)),
|
||||
frappe.bold(flt(d.stock_qty, 2)),
|
||||
),
|
||||
title=_("Insufficient Stock"),
|
||||
)
|
||||
|
||||
def validate_is_pos_using_sales_invoice(self):
|
||||
self.invoice_type_in_pos = frappe.db.get_single_value("POS Settings", "invoice_type")
|
||||
@@ -858,8 +898,6 @@ class POSInvoice(SalesInvoice):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_stock_availability(item_code, warehouse):
|
||||
from erpnext.stock.stock_ledger import is_negative_stock_allowed
|
||||
|
||||
if frappe.db.get_value("Item", item_code, "is_stock_item"):
|
||||
is_stock_item = True
|
||||
bin_qty = get_bin_qty(item_code, warehouse)
|
||||
@@ -876,6 +914,26 @@ def get_stock_availability(item_code, warehouse):
|
||||
return 0, is_stock_item, False
|
||||
|
||||
|
||||
def get_product_bundle_stock_availability(item_code, warehouse, item_qty):
|
||||
is_stock_item = True
|
||||
bundle = frappe.get_doc("Product Bundle", item_code)
|
||||
availabilities = []
|
||||
for bundle_item in bundle.items:
|
||||
if frappe.get_value("Item", bundle_item.item_code, "is_stock_item"):
|
||||
bin_qty = get_bin_qty(bundle_item.item_code, warehouse)
|
||||
reserved_qty = get_pos_reserved_qty(bundle_item.item_code, warehouse)
|
||||
available = bin_qty - reserved_qty
|
||||
availabilities.append(
|
||||
{
|
||||
"item_code": bundle_item.item_code,
|
||||
"required": bundle_item.qty * item_qty,
|
||||
"available": available,
|
||||
}
|
||||
)
|
||||
|
||||
return availabilities, is_stock_item, is_negative_stock_allowed(item_code=item_code)
|
||||
|
||||
|
||||
def get_bundle_availability(bundle_item_code, warehouse):
|
||||
product_bundle = frappe.get_doc("Product Bundle", bundle_item_code)
|
||||
|
||||
|
||||
@@ -1024,6 +1024,84 @@ class TestPOSInvoice(IntegrationTestCase):
|
||||
frappe.db.rollback(save_point="before_test_delivered_serial_no_case")
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
def test_bundle_stock_availability_validation(self):
|
||||
from erpnext.accounts.doctype.pos_invoice.pos_invoice import ProductBundleStockValidationError
|
||||
from erpnext.accounts.doctype.pos_invoice_merge_log.test_pos_invoice_merge_log import (
|
||||
init_user_and_profile,
|
||||
)
|
||||
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
|
||||
init_user_and_profile()
|
||||
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
company = "_Test Company"
|
||||
|
||||
# Create stock sub-items
|
||||
sub_item_a = "_Test Bundle SubA"
|
||||
if not frappe.db.exists("Item", sub_item_a):
|
||||
create_item(
|
||||
item_code=sub_item_a,
|
||||
is_stock_item=1,
|
||||
)
|
||||
|
||||
sub_item_b = "_Test Bundle SubB"
|
||||
if not frappe.db.exists("Item", sub_item_b):
|
||||
create_item(
|
||||
item_code=sub_item_b,
|
||||
is_stock_item=1,
|
||||
)
|
||||
|
||||
# Add initial stock: SubA=5, SubB=2
|
||||
make_stock_entry(item_code=sub_item_a, target=warehouse, qty=5, company=company)
|
||||
make_stock_entry(item_code=sub_item_b, target=warehouse, qty=2, company=company)
|
||||
|
||||
# Create Product Bundle: Test Bundle (SubA x2 + SubB x1)
|
||||
bundle_item = "_Test Bundle"
|
||||
if not frappe.db.exists("Item", bundle_item):
|
||||
create_item(
|
||||
item_code=bundle_item,
|
||||
is_stock_item=0,
|
||||
)
|
||||
|
||||
if not frappe.db.exists("Product Bundle", bundle_item):
|
||||
make_product_bundle(parent=bundle_item, items=[sub_item_a, sub_item_b])
|
||||
|
||||
# Test Case 1: Sufficient stock (bundle qty=1: requires SubA=2 (<=5), SubB=1 (<=2)) -> No error
|
||||
pos_inv_sufficient = create_pos_invoice(
|
||||
item=bundle_item,
|
||||
qty=1,
|
||||
rate=100,
|
||||
warehouse=warehouse,
|
||||
pos_profile=self.pos_profile.name,
|
||||
do_not_save=1,
|
||||
)
|
||||
pos_inv_sufficient.append("payments", {"mode_of_payment": "Cash", "amount": 100, "default": 1})
|
||||
pos_inv_sufficient.insert()
|
||||
pos_inv_sufficient.submit()
|
||||
|
||||
pos_inv_sufficient.cancel()
|
||||
pos_inv_sufficient.delete()
|
||||
|
||||
# Test Case 2: Insufficient stock (reduce SubB to 1, bundle qty=2: requires SubB=2 >1) -> Error with details
|
||||
make_stock_entry(item_code=sub_item_b, from_warehouse=warehouse, qty=1, company=company)
|
||||
|
||||
pos_inv_insufficient = create_pos_invoice(
|
||||
item=bundle_item,
|
||||
qty=2,
|
||||
rate=100,
|
||||
warehouse=warehouse,
|
||||
pos_profile=self.pos_profile.name,
|
||||
do_not_save=1,
|
||||
)
|
||||
pos_inv_insufficient.append("payments", {"mode_of_payment": "Cash", "amount": 200, "default": 1})
|
||||
pos_inv_insufficient.save()
|
||||
self.assertRaises(ProductBundleStockValidationError, pos_inv_insufficient.submit)
|
||||
|
||||
frappe.set_user("test@example.com")
|
||||
|
||||
|
||||
def create_pos_invoice(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -244,7 +244,7 @@ def get_other_conditions(conditions, values, args):
|
||||
conditions += " and " + group_condition
|
||||
|
||||
date = args.get("transaction_date") or frappe.get_value(
|
||||
args.get("doctype"), args.get("name"), "posting_date"
|
||||
args.get("doctype"), args.get("name"), "posting_date", ignore=True
|
||||
)
|
||||
if date:
|
||||
conditions += """ and %(transaction_date)s between ifnull(`tabPricing Rule`.valid_from, '2000-01-01')
|
||||
|
||||
@@ -16,7 +16,7 @@ from erpnext.stock.doctype.item.test_item import create_item
|
||||
class TestProcessDeferredAccounting(IntegrationTestCase):
|
||||
def test_creation_of_ledger_entry_on_submit(self):
|
||||
"""test creation of gl entries on submission of document"""
|
||||
change_acc_settings(acc_frozen_upto="2023-05-31", book_deferred_entries_based_on="Months")
|
||||
change_acc_settings(acc_frozen_till_date="2023-05-31", book_deferred_entries_based_on="Months")
|
||||
|
||||
deferred_account = create_account(
|
||||
account_name="Deferred Revenue for Accounts Frozen",
|
||||
@@ -92,8 +92,10 @@ class TestProcessDeferredAccounting(IntegrationTestCase):
|
||||
pda.cancel()
|
||||
|
||||
|
||||
def change_acc_settings(acc_frozen_upto="", book_deferred_entries_based_on="Days"):
|
||||
def change_acc_settings(
|
||||
company="_Test Company", acc_frozen_till_date=None, book_deferred_entries_based_on="Days"
|
||||
):
|
||||
acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
|
||||
acc_settings.acc_frozen_upto = acc_frozen_upto
|
||||
acc_settings.book_deferred_entries_based_on = book_deferred_entries_based_on
|
||||
frappe.db.set_value("Company", company, "accounts_frozen_till_date", acc_frozen_till_date)
|
||||
acc_settings.save()
|
||||
|
||||
@@ -12,6 +12,7 @@ erpnext.buying.setup_buying_controller();
|
||||
|
||||
erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.BuyingController {
|
||||
setup(doc) {
|
||||
this.setup_accounting_dimension_triggers();
|
||||
this.setup_posting_date_time_check();
|
||||
super.setup(doc);
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
erpnext.selling.SellingController
|
||||
) {
|
||||
setup(doc) {
|
||||
this.setup_accounting_dimension_triggers();
|
||||
this.setup_posting_date_time_check();
|
||||
super.setup(doc);
|
||||
this.frm.make_methods = {
|
||||
|
||||
@@ -282,59 +282,6 @@ class SalesInvoice(SellingController):
|
||||
self.indicator_color = "green"
|
||||
self.indicator_title = _("Paid")
|
||||
|
||||
def before_print(self, settings=None):
|
||||
from frappe.contacts.doctype.address.address import get_address_display_list
|
||||
|
||||
super().before_print(settings)
|
||||
|
||||
company_details = frappe.get_value(
|
||||
"Company", self.company, ["company_logo", "website", "phone_no", "email"], as_dict=True
|
||||
)
|
||||
|
||||
required_fields = [
|
||||
company_details.get("company_logo"),
|
||||
company_details.get("phone_no"),
|
||||
company_details.get("email"),
|
||||
]
|
||||
|
||||
if not all(required_fields) and not frappe.has_permission("Company", "write", throw=False):
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Some required Company details are missing. You don't have permission to update them. Please contact your System Manager."
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if not self.company_address and not frappe.has_permission("Sales Invoice", "write", throw=False):
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Company Address is missing. You don't have permission to update it. Please contact your System Manager."
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
address_display_list = get_address_display_list("Company", self.company)
|
||||
address_line = address_display_list[0].get("address_line1") if address_display_list else ""
|
||||
|
||||
required_fields.append(self.company_address)
|
||||
required_fields.append(address_line)
|
||||
|
||||
if not all(required_fields):
|
||||
frappe.publish_realtime(
|
||||
"sales_invoice_before_print",
|
||||
{
|
||||
"company_logo": company_details.get("company_logo"),
|
||||
"website": company_details.get("website"),
|
||||
"phone_no": company_details.get("phone_no"),
|
||||
"email": company_details.get("email"),
|
||||
"address_line": address_line,
|
||||
"company": self.company,
|
||||
"company_address": self.company_address,
|
||||
"name": self.name,
|
||||
},
|
||||
user=frappe.session.user,
|
||||
)
|
||||
|
||||
def validate(self):
|
||||
self.validate_auto_set_posting_time()
|
||||
super().validate()
|
||||
@@ -1678,7 +1625,11 @@ class SalesInvoice(SellingController):
|
||||
)
|
||||
|
||||
for item in self.get("items"):
|
||||
if flt(item.base_net_amount, item.precision("base_net_amount")) or item.is_fixed_asset:
|
||||
if (
|
||||
flt(item.base_net_amount, item.precision("base_net_amount"))
|
||||
or item.is_fixed_asset
|
||||
or enable_discount_accounting
|
||||
):
|
||||
# Do not book income for transfer within same company
|
||||
if self.is_internal_transfer():
|
||||
continue
|
||||
@@ -2948,59 +2899,6 @@ def get_loyalty_programs(customer):
|
||||
return lp_details
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def save_company_master_details(name, company, details):
|
||||
from frappe.utils import validate_email_address
|
||||
|
||||
if isinstance(details, str):
|
||||
details = frappe.parse_json(details)
|
||||
|
||||
if details.get("email"):
|
||||
validate_email_address(details.get("email"), throw=True)
|
||||
|
||||
company_fields = ["company_logo", "website", "phone_no", "email"]
|
||||
company_fields_to_update = {field: details.get(field) for field in company_fields if details.get(field)}
|
||||
|
||||
if company_fields_to_update:
|
||||
frappe.db.set_value("Company", company, company_fields_to_update)
|
||||
|
||||
company_address = details.get("company_address")
|
||||
if details.get("address_line1"):
|
||||
address_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Address",
|
||||
"address_title": details.get("address_title"),
|
||||
"address_type": details.get("address_type"),
|
||||
"address_line1": details.get("address_line1"),
|
||||
"address_line2": details.get("address_line2"),
|
||||
"city": details.get("city"),
|
||||
"state": details.get("state"),
|
||||
"pincode": details.get("pincode"),
|
||||
"country": details.get("country"),
|
||||
"is_your_company_address": 1,
|
||||
"links": [{"link_doctype": "Company", "link_name": company}],
|
||||
}
|
||||
)
|
||||
address_doc.insert()
|
||||
company_address = address_doc.name
|
||||
|
||||
if company_address:
|
||||
company_address_display = frappe.db.get_value("Sales Invoice", name, "company_address_display")
|
||||
if not company_address_display or details.get("address_line1"):
|
||||
from frappe.query_builder import DocType
|
||||
|
||||
SalesInvoice = DocType("Sales Invoice")
|
||||
|
||||
(
|
||||
frappe.qb.update(SalesInvoice)
|
||||
.set(SalesInvoice.company_address, company_address)
|
||||
.set(SalesInvoice.company_address_display, get_address_display(company_address))
|
||||
.where(SalesInvoice.name == name)
|
||||
).run()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_invoice_discounting(source_name, target_doc=None):
|
||||
invoice = frappe.get_doc("Sales Invoice", source_name)
|
||||
|
||||
@@ -63,7 +63,8 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
set_default_account_for_mode_of_payment(
|
||||
mode_of_payment, "_Test Company with perpetual inventory", "_Test Bank - TCP1"
|
||||
)
|
||||
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
|
||||
for company in frappe.get_all("Company", pluck="name"):
|
||||
frappe.db.set_value("Company", company, "accounts_frozen_till_date", None)
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
@@ -3398,8 +3399,8 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
si.commission_rate = commission_rate
|
||||
self.assertRaises(frappe.ValidationError, si.save)
|
||||
|
||||
@IntegrationTestCase.change_settings("Accounts Settings", {"acc_frozen_upto": add_days(getdate(), 1)})
|
||||
def test_sales_invoice_submission_post_account_freezing_date(self):
|
||||
frappe.db.set_value("Company", "_Test Company", "accounts_frozen_till_date", add_days(getdate(), 1))
|
||||
si = create_sales_invoice(do_not_save=True)
|
||||
si.posting_date = add_days(getdate(), 1)
|
||||
si.save()
|
||||
@@ -3407,6 +3408,7 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
self.assertRaises(frappe.ValidationError, si.submit)
|
||||
si.posting_date = getdate()
|
||||
si.submit()
|
||||
frappe.db.set_value("Company", "_Test Company", "accounts_frozen_till_date", None)
|
||||
|
||||
@IntegrationTestCase.change_settings("Accounts Settings", {"over_billing_allowance": 0})
|
||||
def test_over_billing_case_against_delivery_note(self):
|
||||
@@ -3473,7 +3475,7 @@ class TestSalesInvoice(ERPNextTestSuite):
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", getdate("2019-01-31"))
|
||||
frappe.db.set_value("Company", "_Test Company", "accounts_frozen_till_date", getdate("2019-01-31"))
|
||||
|
||||
pda1 = frappe.get_doc(
|
||||
dict(
|
||||
|
||||
@@ -27,7 +27,7 @@ class TestSubscription(IntegrationTestCase):
|
||||
make_plans()
|
||||
create_parties()
|
||||
reset_settings()
|
||||
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
|
||||
frappe.db.set_value("Company", "_Test Company", "accounts_frozen_till_date", None)
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
@@ -301,7 +301,9 @@ def merge_similar_entries(gl_map, precision=None):
|
||||
company_currency = erpnext.get_company_currency(company)
|
||||
|
||||
if not precision:
|
||||
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency)
|
||||
precision = get_field_precision(
|
||||
frappe.get_meta("GL Entry").get_field("debit"), currency=company_currency
|
||||
)
|
||||
|
||||
# filter zero debit and credit entries
|
||||
merged_gl_map = filter(
|
||||
@@ -404,7 +406,7 @@ def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
|
||||
|
||||
dimension_filter_map = get_dimension_filter_map()
|
||||
if gl_map:
|
||||
check_freezing_date(gl_map[0]["posting_date"], adv_adj)
|
||||
check_freezing_date(gl_map[0]["posting_date"], gl_map[0]["company"], adv_adj)
|
||||
is_opening = any(d.get("is_opening") == "Yes" for d in gl_map)
|
||||
if gl_map[0]["voucher_type"] != "Period Closing Voucher":
|
||||
validate_against_pcv(is_opening, gl_map[0]["posting_date"], gl_map[0]["company"])
|
||||
@@ -765,7 +767,7 @@ def make_reverse_gl_entries(
|
||||
make_entry(new_gle, adv_adj, "Yes")
|
||||
|
||||
|
||||
def check_freezing_date(posting_date, adv_adj=False):
|
||||
def check_freezing_date(posting_date, company, adv_adj=False):
|
||||
"""
|
||||
Nobody can do GL Entries where posting date is before freezing date
|
||||
except authorized person
|
||||
@@ -774,17 +776,17 @@ def check_freezing_date(posting_date, adv_adj=False):
|
||||
Hence stop admin to bypass if accounts are freezed
|
||||
"""
|
||||
if not adv_adj:
|
||||
acc_frozen_upto = frappe.get_single_value("Accounts Settings", "acc_frozen_upto")
|
||||
if acc_frozen_upto:
|
||||
frozen_accounts_modifier = frappe.get_single_value(
|
||||
"Accounts Settings", "frozen_accounts_modifier"
|
||||
acc_frozen_till_date = frappe.db.get_value("Company", company, "accounts_frozen_till_date")
|
||||
if acc_frozen_till_date:
|
||||
frozen_accounts_modifier = frappe.db.get_value(
|
||||
"Company", company, "role_allowed_for_frozen_entries"
|
||||
)
|
||||
if getdate(posting_date) <= getdate(acc_frozen_upto) and (
|
||||
if getdate(posting_date) <= getdate(acc_frozen_till_date) and (
|
||||
frozen_accounts_modifier not in frappe.get_roles() or frappe.session.user == "Administrator"
|
||||
):
|
||||
frappe.throw(
|
||||
_("You are not authorized to add or update entries before {0}").format(
|
||||
formatdate(acc_frozen_upto)
|
||||
formatdate(acc_frozen_till_date)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,108 +1,108 @@
|
||||
<style>
|
||||
.letter-head {
|
||||
border-radius: 18px;
|
||||
padding-right: 12px;
|
||||
margin-left: 12px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.letter-head {
|
||||
border-radius: 18px;
|
||||
padding-right: 12px;
|
||||
margin-left: 12px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.letter-head td{
|
||||
padding: 0px !important;
|
||||
}
|
||||
.invoice-header {
|
||||
width: 100%;
|
||||
}
|
||||
.logo-cell {
|
||||
width: 100px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
.logo-container {
|
||||
width: 90px;
|
||||
display: block;
|
||||
}
|
||||
.logo-container img {
|
||||
max-width: 90px;
|
||||
max-height: 90px;
|
||||
display: inline-block;
|
||||
border-radius: 15px;
|
||||
}
|
||||
.company-details {
|
||||
width: 40%;
|
||||
align-content: center;
|
||||
}
|
||||
.company-name {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #171717;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.invoice-info-cell {
|
||||
float: right;
|
||||
vertical-align: top;
|
||||
}
|
||||
.invoice-info {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.invoice-label {
|
||||
color: #7C7C7C;
|
||||
display: inline-block;
|
||||
width: 60px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.letter-head td {
|
||||
padding: 0px !important;
|
||||
}
|
||||
.invoice-header {
|
||||
width: 100%;
|
||||
}
|
||||
.logo-cell {
|
||||
width: 100px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
.logo-container {
|
||||
width: 90px;
|
||||
display: block;
|
||||
}
|
||||
.logo-container img {
|
||||
max-width: 90px;
|
||||
max-height: 90px;
|
||||
display: inline-block;
|
||||
border-radius: 15px;
|
||||
}
|
||||
.company-details {
|
||||
width: 40%;
|
||||
align-content: center;
|
||||
}
|
||||
.company-name {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #171717;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.invoice-info-cell {
|
||||
float: right;
|
||||
vertical-align: top;
|
||||
}
|
||||
.invoice-info {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.invoice-label {
|
||||
color: #7c7c7c;
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<table class="invoice-header">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="logo-cell" style="vertical-align: middle !important;">
|
||||
<div class="logo-container">
|
||||
{% set company_logo = frappe.db.get_value("Company", doc.company, "company_logo") %}
|
||||
{% if company_logo %}
|
||||
<img src="{{ frappe.utils.get_url(company_logo) }}" alt="Company Logo">
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="logo-cell" style="vertical-align: middle !important">
|
||||
<div class="logo-container">
|
||||
{% set company_logo = frappe.db.get_value("Company", doc.company, "company_logo") %} {% if
|
||||
company_logo %}
|
||||
<img src="{{ frappe.utils.get_url(company_logo) }}" alt="Company Logo" />
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="company-details">
|
||||
<div class="company-name">
|
||||
{{ doc.company }}
|
||||
</div>
|
||||
{% if doc.company_address %}
|
||||
{% set company_address = frappe.db.get_value("Address", doc.company_address, ["address_line1", "address_line2", "city", "state", "pincode", "country"], as_dict=True) %}
|
||||
<td class="company-details">
|
||||
<div class="company-name">{{ doc.company }}</div>
|
||||
{% if doc.company_address %} {% set company_address = frappe.db.get_value("Address",
|
||||
doc.company_address, ["address_line1", "address_line2", "city", "state", "pincode",
|
||||
"country"], as_dict=True) %} {% elif doc.billing_address %} {% set company_address =
|
||||
frappe.db.get_value("Address", doc.billing_address, ["address_line1", "address_line2", "city",
|
||||
"state", "pincode", "country"], as_dict=True) %} {% endif %} {% if company_address %} {{
|
||||
company_address.address_line1 or "" }}<br />
|
||||
{% if company_address.address_line2 %} {{ company_address.address_line2 }}<br />
|
||||
{% endif %} {{ company_address.city or "" }}, {{ company_address.state or "" }} {{
|
||||
company_address.pincode or "" }}, {{ company_address.country or ""}}<br />
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
{{ company_address.get("address_line1") or "" }}<br>
|
||||
{% if company_address.get("address_line2") %}{{ company_address.get("address_line2") }}<br>{% endif %}
|
||||
{{ company_address.get("city") or "" }}, {{ company_address.get("state") or "" }} {{ company_address.get("pincode") or "" }}, {{ company_address.get("country") or "" }}<br>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="invoice-info-cell">
|
||||
{% set website = frappe.db.get_value("Company", doc.company, "website") %} {% set email =
|
||||
frappe.db.get_value("Company", doc.company, "email") %} {% set phone_no =
|
||||
frappe.db.get_value("Company", doc.company, "phone_no") %}
|
||||
|
||||
<td class="invoice-info-cell">
|
||||
{% set company_details = frappe.db.get_value("Company", doc.company, ["website", "email", "phone_no"], as_dict=True) %}
|
||||
|
||||
<div class="invoice-info">
|
||||
<span class="invoice-label">{{ _("Invoice:") }}</span>
|
||||
<span>{{ doc.name }}</span>
|
||||
</div>
|
||||
{% if company_details.website %}
|
||||
<div class="invoice-info">
|
||||
<span class="invoice-label">{{ _("Website:") }}</span>
|
||||
<span>{{ company_details.website }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if company_details.email %}
|
||||
<div class="invoice-info">
|
||||
<span class="invoice-label">{{ _("Email:") }}</span>
|
||||
<span>{{ company_details.email }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if company_details.phone_no %}
|
||||
<div class="invoice-info">
|
||||
<span class="invoice-label">{{ _("Contact:") }}</span>
|
||||
<span>{{ company_details.phone_no }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<div class="invoice-info">
|
||||
<span class="invoice-label">{{ doc.doctype }}</span>
|
||||
<span>{{ doc.name }}</span>
|
||||
</div>
|
||||
{% if website %}
|
||||
<div class="invoice-info">
|
||||
<span class="invoice-label">{{ _("Website:") }}</span>
|
||||
<span>{{ website }}</span>
|
||||
</div>
|
||||
{% endif %} {% if email %}
|
||||
<div class="invoice-info">
|
||||
<span class="invoice-label">{{ _("Email:") }}</span>
|
||||
<span>{{ email }}</span>
|
||||
</div>
|
||||
{% endif %} {% if phone_no %}
|
||||
<div class="invoice-info">
|
||||
<span class="invoice-label">{{ _("Contact:") }}</span>
|
||||
<span>{{ phone_no }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<style>
|
||||
.print-format-preview {
|
||||
margin-top: 12px;
|
||||
}
|
||||
margin-top: 12px;
|
||||
}
|
||||
.letter-head {
|
||||
border-radius: 18px;
|
||||
background: #f8f8f8;
|
||||
@@ -82,41 +82,43 @@
|
||||
{% endif %}
|
||||
<div class="company-name">{{ doc.company }}</div>
|
||||
<div class="company-address">
|
||||
{% if doc.company_address %}
|
||||
{% set company_address = frappe.db.get_value("Address", doc.company_address, ["address_line1", "address_line2", "city", "state", "pincode", "country"], as_dict=True) %}
|
||||
{{ company_address.address_line1 or "" }}<br />
|
||||
{% if company_address.address_line2 %} {{ company_address.address_line2 }}<br /> {% endif %}
|
||||
{{ company_address.city or "" }}, {{ company_address.state or "" }}
|
||||
{{ company_address.pincode or "" }}, {{ company_address.country or ""}}<br />
|
||||
{% if doc.company_address %} {% set company_address = frappe.db.get_value("Address",
|
||||
doc.company_address, ["address_line1", "address_line2", "city", "state", "pincode",
|
||||
"country"], as_dict=True) %} {% elif doc.billing_address %} {% set company_address =
|
||||
frappe.db.get_value("Address", doc.billing_address, ["address_line1", "address_line2",
|
||||
"city", "state", "pincode", "country"], as_dict=True) %} {% endif %} {% if company_address
|
||||
%} {{ company_address.address_line1 or "" }}<br />
|
||||
{% if company_address.address_line2 %} {{ company_address.address_line2 }}<br />
|
||||
{% endif %} {{ company_address.city or "" }}, {{ company_address.state or "" }} {{
|
||||
company_address.pincode or "" }}, {{ company_address.country or ""}}<br />
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td style="vertical-align: top">
|
||||
<div style="height: 90px; margin-bottom: 10px; text-align: right">
|
||||
<div class="invoice-title">{{ _("Sales Invoice") }}</div>
|
||||
<div class="invoice-title">{{ doc.doctype }}</div>
|
||||
<div class="invoice-number">{{ doc.name }}</div>
|
||||
<br />
|
||||
</div>
|
||||
<div style="text-align: left; float: right" class="other-details">
|
||||
{% set company_details = frappe.db.get_value("Company", doc.company, ["website", "email", "phone_no"], as_dict=True) %}
|
||||
{% if company_details.website %}
|
||||
<div>
|
||||
<span class="contact-title">{{ _("Website:") }}</span
|
||||
><span class="contact-value">{{ company_details.website }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if company_details.email %}
|
||||
<div>
|
||||
<span class="contact-title">{{ _("Email:") }}</span
|
||||
><span class="contact-value">{{ company_details.email }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if company_details.phone_no %}
|
||||
<div>
|
||||
<span class="contact-title">{{ _("Contact:") }}</span
|
||||
><span class="contact-value">{{ company_details.phone_no }}</span>
|
||||
</div>
|
||||
{% set company_details = frappe.db.get_value("Company", doc.company, ["website", "email",
|
||||
"phone_no"], as_dict=True) %} {% set website = company_details.website %} {% set email =
|
||||
company_details.email %} {% set phone_no = company_details.phone_no %} {% if website %}
|
||||
<div>
|
||||
<span class="contact-title">{{ _("Website:") }}</span
|
||||
><span class="contact-value">{{ website }}</span>
|
||||
</div>
|
||||
{% endif %} {% if email %}
|
||||
<div>
|
||||
<span class="contact-title">{{ _("Email:") }}</span
|
||||
><span class="contact-value">{{ email }}</span>
|
||||
</div>
|
||||
{% endif %} {% if phone_no %}
|
||||
<div>
|
||||
<span class="contact-title">{{ _("Contact:") }}</span
|
||||
><span class="contact-value">{{ phone_no }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -795,7 +795,7 @@ def get_payment_terms_template(party_name, party_type, company=None):
|
||||
return template
|
||||
|
||||
|
||||
def validate_party_frozen_disabled(party_type, party_name):
|
||||
def validate_party_frozen_disabled(company, party_type, party_name):
|
||||
if frappe.flags.ignore_party_validation:
|
||||
return
|
||||
|
||||
@@ -805,10 +805,10 @@ def validate_party_frozen_disabled(party_type, party_name):
|
||||
if party.disabled:
|
||||
frappe.throw(_("{0} {1} is disabled").format(party_type, party_name), PartyDisabled)
|
||||
elif party.get("is_frozen"):
|
||||
frozen_accounts_modifier = frappe.get_single_value(
|
||||
"Accounts Settings", "frozen_accounts_modifier"
|
||||
role_allowed_for_frozen_entries = frappe.get_cached_value(
|
||||
"Company", company, "role_allowed_for_frozen_entries"
|
||||
)
|
||||
if frozen_accounts_modifier not in frappe.get_roles():
|
||||
if role_allowed_for_frozen_entries not in frappe.get_roles():
|
||||
frappe.throw(_("{0} {1} is frozen").format(party_type, party_name), PartyFrozen)
|
||||
|
||||
elif party_type == "Employee":
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -102,6 +102,11 @@ frappe.query_reports["Accounts Payable Summary"] = {
|
||||
label: __("Revaluation Journals"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
{
|
||||
fieldname: "show_gl_balance",
|
||||
label: __("Show GL Balance"),
|
||||
fieldtype: "Check",
|
||||
},
|
||||
],
|
||||
|
||||
onload: function (report) {
|
||||
|
||||
@@ -53,7 +53,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
)
|
||||
|
||||
if self.filters.show_gl_balance:
|
||||
gl_balance_map = get_gl_balance(self.filters.report_date, self.filters.company)
|
||||
gl_balance_map = get_gl_balance(self.filters.report_date, self.filters.company, self.account_type)
|
||||
|
||||
for party, party_dict in self.party_total.items():
|
||||
if flt(party_dict.outstanding, self.currency_precision) == 0:
|
||||
@@ -206,11 +206,15 @@ class AccountsReceivableSummary(ReceivablePayableReport):
|
||||
)
|
||||
|
||||
|
||||
def get_gl_balance(report_date, company):
|
||||
def get_gl_balance(report_date, company, account_type):
|
||||
if account_type == "Payable":
|
||||
balance_calc_fields = ["party", {"SUM": [{"SUB": ["credit", "debit"]}], "as": "balance"}]
|
||||
else:
|
||||
balance_calc_fields = ["party", {"SUM": [{"SUB": ["debit", "credit"]}], "as": "balance"}]
|
||||
return frappe._dict(
|
||||
frappe.db.get_all(
|
||||
"GL Entry",
|
||||
fields=["party", {"SUM": [{"SUB": ["debit", "credit"]}], "as": "balance"}],
|
||||
fields=balance_calc_fields,
|
||||
filters={"posting_date": ("<=", report_date), "is_cancelled": 0, "company": company},
|
||||
group_by="party",
|
||||
as_list=1,
|
||||
|
||||
@@ -69,12 +69,18 @@ class PartyLedgerSummaryReport:
|
||||
party_type = self.filters.party_type
|
||||
|
||||
doctype = qb.DocType(party_type)
|
||||
|
||||
party_details_fields = [
|
||||
doctype.name.as_("party"),
|
||||
f"{scrub(party_type)}_name",
|
||||
f"{scrub(party_type)}_group",
|
||||
]
|
||||
|
||||
if party_type == "Customer":
|
||||
party_details_fields.append(doctype.territory)
|
||||
|
||||
conditions = self.get_party_conditions(doctype)
|
||||
query = (
|
||||
qb.from_(doctype)
|
||||
.select(doctype.name.as_("party"), f"{scrub(party_type)}_name")
|
||||
.where(Criterion.all(conditions))
|
||||
)
|
||||
query = qb.from_(doctype).select(*party_details_fields).where(Criterion.all(conditions))
|
||||
|
||||
from frappe.desk.reportview import build_match_conditions
|
||||
|
||||
@@ -153,6 +159,31 @@ class PartyLedgerSummaryReport:
|
||||
|
||||
credit_or_debit_note = "Credit Note" if self.filters.party_type == "Customer" else "Debit Note"
|
||||
|
||||
if self.filters.party_type == "Customer":
|
||||
columns += [
|
||||
{
|
||||
"label": _("Customer Group"),
|
||||
"fieldname": "customer_group",
|
||||
"fieldtype": "Link",
|
||||
"options": "Customer Group",
|
||||
},
|
||||
{
|
||||
"label": _("Territory"),
|
||||
"fieldname": "territory",
|
||||
"fieldtype": "Link",
|
||||
"options": "Territory",
|
||||
},
|
||||
]
|
||||
else:
|
||||
columns += [
|
||||
{
|
||||
"label": _("Supplier Group"),
|
||||
"fieldname": "supplier_group",
|
||||
"fieldtype": "Link",
|
||||
"options": "Supplier Group",
|
||||
}
|
||||
]
|
||||
|
||||
columns += [
|
||||
{
|
||||
"label": _("Opening Balance"),
|
||||
@@ -214,35 +245,6 @@ class PartyLedgerSummaryReport:
|
||||
},
|
||||
]
|
||||
|
||||
# Hidden columns for handling 'User Permissions'
|
||||
if self.filters.party_type == "Customer":
|
||||
columns += [
|
||||
{
|
||||
"label": _("Territory"),
|
||||
"fieldname": "territory",
|
||||
"fieldtype": "Link",
|
||||
"options": "Territory",
|
||||
"hidden": 1,
|
||||
},
|
||||
{
|
||||
"label": _("Customer Group"),
|
||||
"fieldname": "customer_group",
|
||||
"fieldtype": "Link",
|
||||
"options": "Customer Group",
|
||||
"hidden": 1,
|
||||
},
|
||||
]
|
||||
else:
|
||||
columns += [
|
||||
{
|
||||
"label": _("Supplier Group"),
|
||||
"fieldname": "supplier_group",
|
||||
"fieldtype": "Link",
|
||||
"options": "Supplier Group",
|
||||
"hidden": 1,
|
||||
}
|
||||
]
|
||||
|
||||
columns.append({"label": _("Dr/Cr"), "fieldname": "dr_or_cr", "fieldtype": "Data", "width": 100})
|
||||
return columns
|
||||
|
||||
|
||||
@@ -186,6 +186,8 @@ class TestCustomerLedgerSummary(AccountsTestMixin, IntegrationTestCase):
|
||||
expected = {
|
||||
"party": "_Test Customer",
|
||||
"customer_name": "_Test Customer",
|
||||
"customer_group": "_Test Customer Group",
|
||||
"territory": "_Test Territory",
|
||||
"party_name": "_Test Customer",
|
||||
"opening_balance": 0,
|
||||
"invoiced_amount": 100.0,
|
||||
@@ -213,6 +215,8 @@ class TestCustomerLedgerSummary(AccountsTestMixin, IntegrationTestCase):
|
||||
expected = {
|
||||
"party": "_Test Customer",
|
||||
"customer_name": "_Test Customer",
|
||||
"customer_group": "_Test Customer Group",
|
||||
"territory": "_Test Territory",
|
||||
"party_name": "_Test Customer",
|
||||
"opening_balance": 0,
|
||||
"invoiced_amount": 100.0,
|
||||
|
||||
@@ -198,7 +198,7 @@ frappe.ui.form.on("Asset", {
|
||||
callback: function (r) {
|
||||
if (!r.message) {
|
||||
$(".primary-action").prop("hidden", true);
|
||||
$(".form-message").text("Capitalize this asset to confirm");
|
||||
$(".form-message").text(__("Capitalize this asset to confirm"));
|
||||
|
||||
frm.add_custom_button(__("Capitalize Asset"), function () {
|
||||
frm.trigger("create_asset_capitalization");
|
||||
|
||||
@@ -7,6 +7,7 @@ import math
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import IfNull, Sum
|
||||
from frappe.utils import (
|
||||
cint,
|
||||
flt,
|
||||
@@ -121,7 +122,7 @@ class Asset(AccountsController):
|
||||
def validate(self):
|
||||
self.validate_category()
|
||||
self.validate_precision()
|
||||
self.validate_linked_purchase_docs()
|
||||
self.validate_linked_purchase_documents()
|
||||
self.set_purchase_doc_row_item()
|
||||
self.validate_asset_values()
|
||||
self.validate_asset_and_reference()
|
||||
@@ -151,9 +152,9 @@ class Asset(AccountsController):
|
||||
if not schedule_doc:
|
||||
schedule_doc = frappe.new_doc("Asset Depreciation Schedule")
|
||||
schedule_doc.asset = self.name
|
||||
schedule_doc.create_depreciation_schedule(row)
|
||||
schedule_doc.save()
|
||||
schedules.append(schedule_doc.name)
|
||||
schedule_doc.create_depreciation_schedule(row)
|
||||
schedule_doc.save()
|
||||
schedules.append(schedule_doc.name)
|
||||
|
||||
self.show_schedule_creation_message(schedules)
|
||||
|
||||
@@ -422,20 +423,67 @@ class Asset(AccountsController):
|
||||
if self.available_for_use_date and getdate(self.available_for_use_date) < getdate(self.purchase_date):
|
||||
frappe.throw(_("Available-for-use Date should be after purchase date"))
|
||||
|
||||
def validate_linked_purchase_docs(self):
|
||||
for doctype_field, doctype_name in [
|
||||
def validate_linked_purchase_documents(self):
|
||||
for fieldname, doctype in [
|
||||
("purchase_receipt", "Purchase Receipt"),
|
||||
("purchase_invoice", "Purchase Invoice"),
|
||||
]:
|
||||
linked_doc = getattr(self, doctype_field, None)
|
||||
if linked_doc:
|
||||
docstatus = frappe.db.get_value(doctype_name, linked_doc, "docstatus")
|
||||
if docstatus == 0:
|
||||
frappe.throw(
|
||||
_("{0} is still in Draft. Please submit it before saving the Asset.").format(
|
||||
get_link_to_form(doctype_name, linked_doc)
|
||||
)
|
||||
purchase_doc = getattr(self, fieldname, None)
|
||||
|
||||
if not purchase_doc:
|
||||
continue
|
||||
|
||||
if frappe.db.get_value(doctype, purchase_doc, "docstatus") == 0:
|
||||
frappe.throw(
|
||||
_("{0} is in Draft. Submit it before creating the Asset.").format(
|
||||
get_link_to_form(doctype, purchase_doc)
|
||||
)
|
||||
)
|
||||
|
||||
self.validate_asset_qty_with_purchase_doc(doctype, purchase_doc)
|
||||
|
||||
def validate_asset_qty_with_purchase_doc(self, doctype, purchase_doc):
|
||||
Asset = frappe.qb.DocType("Asset")
|
||||
|
||||
if doctype == "Purchase Invoice":
|
||||
asset_filter = Asset.purchase_invoice == purchase_doc
|
||||
else:
|
||||
asset_filter = Asset.purchase_receipt == purchase_doc
|
||||
|
||||
existing_asset_qty = (
|
||||
frappe.qb.from_(Asset)
|
||||
.select(IfNull(Sum(Asset.asset_quantity), 0))
|
||||
.where((Asset.item_code == self.item_code) & (Asset.name != self.name) & (Asset.docstatus != 2))
|
||||
.where(asset_filter)
|
||||
).run()[0][0]
|
||||
|
||||
PurchaseDoc = frappe.qb.DocType(doctype)
|
||||
PurchaseDocItems = frappe.qb.DocType(f"{doctype} Item")
|
||||
|
||||
purchased_qty = (
|
||||
frappe.qb.from_(PurchaseDoc)
|
||||
.join(PurchaseDocItems)
|
||||
.on(PurchaseDoc.name == PurchaseDocItems.parent)
|
||||
.select(IfNull(Sum(PurchaseDocItems.qty), 0))
|
||||
.where(PurchaseDoc.name == purchase_doc)
|
||||
.where(PurchaseDocItems.item_code == self.item_code)
|
||||
).run()[0][0]
|
||||
|
||||
if (existing_asset_qty + self.asset_quantity) > purchased_qty:
|
||||
frappe.throw(
|
||||
_(
|
||||
"<b>Cannot create asset.</b><br><br>"
|
||||
"You're trying to create <b>{0} asset(s)</b> from {2} {3}.<br>"
|
||||
"However, only <b>{1} item(s)</b> were purchased and <b>{4} asset(s)</b> already exist against {5}."
|
||||
).format(
|
||||
self.asset_quantity,
|
||||
purchased_qty,
|
||||
doctype,
|
||||
get_link_to_form(doctype, purchase_doc),
|
||||
existing_asset_qty,
|
||||
purchase_doc,
|
||||
)
|
||||
)
|
||||
|
||||
def validate_gross_and_purchase_amount(self):
|
||||
if self.is_existing_asset:
|
||||
@@ -1212,7 +1260,7 @@ def get_values_from_purchase_doc(purchase_doc_name, item_code, doctype):
|
||||
|
||||
return {
|
||||
"company": purchase_doc.company,
|
||||
"purchase_date": purchase_doc.get("bill_date") or purchase_doc.get("posting_date"),
|
||||
"purchase_date": purchase_doc.get("posting_date"),
|
||||
"net_purchase_amount": flt(first_item.base_net_amount),
|
||||
"asset_quantity": first_item.qty,
|
||||
"cost_center": first_item.cost_center or purchase_doc.get("cost_center"),
|
||||
|
||||
@@ -96,13 +96,28 @@ def get_depreciable_assets_data(date):
|
||||
.orderby(a.creation, order=Order.desc)
|
||||
)
|
||||
|
||||
acc_frozen_upto = get_acc_frozen_upto()
|
||||
if acc_frozen_upto:
|
||||
res = res.where(ds.schedule_date > acc_frozen_upto)
|
||||
companies_with_frozen_limits = get_companies_with_frozen_limits()
|
||||
|
||||
res = res.run()
|
||||
for company, frozen_upto in companies_with_frozen_limits.items():
|
||||
res = res.where((a.company != company) | (ds.schedule_date > frozen_upto))
|
||||
|
||||
return res
|
||||
return res.run()
|
||||
|
||||
|
||||
def get_companies_with_frozen_limits():
|
||||
companies_with_frozen_limits = {}
|
||||
for d in frappe.get_all(
|
||||
"Company", fields=["name", "accounts_frozen_till_date", "role_allowed_for_frozen_entries"]
|
||||
):
|
||||
if not d.accounts_frozen_till_date:
|
||||
continue
|
||||
|
||||
if (
|
||||
d.role_allowed_for_frozen_entries not in frappe.get_roles()
|
||||
and frappe.session.user != "Administrator"
|
||||
):
|
||||
companies_with_frozen_limits[d.name] = getdate(d.accounts_frozen_till_date)
|
||||
return companies_with_frozen_limits
|
||||
|
||||
|
||||
def make_depreciation_entry_on_disposal(asset_doc, disposal_date=None):
|
||||
@@ -111,20 +126,6 @@ def make_depreciation_entry_on_disposal(asset_doc, disposal_date=None):
|
||||
make_depreciation_entry(depr_schedule_name, disposal_date)
|
||||
|
||||
|
||||
def get_acc_frozen_upto():
|
||||
acc_frozen_upto = frappe.get_single_value("Accounts Settings", "acc_frozen_upto")
|
||||
|
||||
if not acc_frozen_upto:
|
||||
return
|
||||
|
||||
frozen_accounts_modifier = frappe.get_single_value("Accounts Settings", "frozen_accounts_modifier")
|
||||
|
||||
if frozen_accounts_modifier not in frappe.get_roles() or frappe.session.user == "Administrator":
|
||||
return getdate(acc_frozen_upto)
|
||||
|
||||
return
|
||||
|
||||
|
||||
def get_credit_debit_accounts_for_asset(asset_category, company):
|
||||
# Returns credit and debit accounts for the given asset category and company.
|
||||
(_, accumulated_depr_account, depr_expense_account) = get_depreciation_accounts(asset_category, company)
|
||||
@@ -307,7 +308,8 @@ def set_depr_entry_posting_status_for_failed_assets(failed_asset_names):
|
||||
|
||||
|
||||
def notify_depr_entry_posting_error(failed_asset_names, error_log_names):
|
||||
recipients = get_users_with_role("Accounts Manager")
|
||||
user_role = frappe.db.get_single_value("Accounts Settings", "role_to_notify_on_depreciation_failure")
|
||||
recipients = get_users_with_role(user_role or "Accounts Manager")
|
||||
|
||||
if not recipients:
|
||||
recipients = get_users_with_role("System Manager")
|
||||
|
||||
@@ -73,14 +73,7 @@ frappe.ui.form.on("Asset Repair", {
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
if (frm.doc.docstatus) {
|
||||
frm.add_custom_button(__("View General Ledger"), function () {
|
||||
frappe.route_options = {
|
||||
voucher_no: frm.doc.name,
|
||||
};
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
});
|
||||
}
|
||||
frm.events.show_general_ledger(frm);
|
||||
|
||||
let sbb_field = frm.get_docfield("stock_items", "serial_and_batch_bundle");
|
||||
if (sbb_field) {
|
||||
@@ -144,6 +137,26 @@ frappe.ui.form.on("Asset Repair", {
|
||||
frm.set_value("repair_cost", 0);
|
||||
}
|
||||
},
|
||||
|
||||
show_general_ledger: (frm) => {
|
||||
if (frm.doc.docstatus > 0) {
|
||||
frm.add_custom_button(
|
||||
__("Accounting Ledger"),
|
||||
function () {
|
||||
frappe.route_options = {
|
||||
voucher_no: frm.doc.name,
|
||||
from_date: frm.doc.posting_date,
|
||||
to_date: moment(frm.doc.modified).format("YYYY-MM-DD"),
|
||||
company: frm.doc.company,
|
||||
categorize_by: "",
|
||||
show_cancelled_entries: frm.doc.docstatus === 2,
|
||||
};
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
},
|
||||
__("View")
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Asset Repair Consumed Item", {
|
||||
|
||||
@@ -260,8 +260,13 @@
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-11-04 23:06:43.644846",
|
||||
"links": [
|
||||
{
|
||||
"link_doctype": "Stock Entry",
|
||||
"link_fieldname": "asset_repair"
|
||||
}
|
||||
],
|
||||
"modified": "2025-11-28 13:04:34.921098",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Repair",
|
||||
|
||||
@@ -7,6 +7,9 @@ from frappe.query_builder import DocType
|
||||
from frappe.utils import cint, flt, get_link_to_form, getdate, time_diff_in_hours
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_accounting_dimensions,
|
||||
)
|
||||
from erpnext.accounts.general_ledger import make_gl_entries
|
||||
from erpnext.assets.doctype.asset.asset import get_asset_account
|
||||
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
|
||||
@@ -231,6 +234,12 @@ class AssetRepair(AccountsController):
|
||||
}
|
||||
)
|
||||
|
||||
accounting_dimensions = {
|
||||
"cost_center": self.cost_center,
|
||||
"project": self.project,
|
||||
**{dimension: self.get(dimension) for dimension in get_accounting_dimensions()},
|
||||
}
|
||||
|
||||
for stock_item in self.get("stock_items"):
|
||||
self.validate_serial_no(stock_item)
|
||||
|
||||
@@ -242,8 +251,7 @@ class AssetRepair(AccountsController):
|
||||
"qty": stock_item.consumed_quantity,
|
||||
"basic_rate": stock_item.valuation_rate,
|
||||
"serial_and_batch_bundle": stock_item.serial_and_batch_bundle,
|
||||
"cost_center": self.cost_center,
|
||||
"project": self.project,
|
||||
**accounting_dimensions,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -320,7 +328,8 @@ class AssetRepair(AccountsController):
|
||||
"voucher_no": self.name,
|
||||
"cost_center": self.cost_center,
|
||||
"posting_date": self.completion_date,
|
||||
"against_voucher_type": "Purchase Invoice",
|
||||
"against_voucher_type": "Asset",
|
||||
"against_voucher": self.asset,
|
||||
"company": self.company,
|
||||
},
|
||||
item=self,
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.utils import add_days, add_months, flt, get_first_day, nowdate, nowtime, today
|
||||
|
||||
@@ -322,6 +324,31 @@ class TestAssetRepair(IntegrationTestCase):
|
||||
stock_entry = frappe.get_last_doc("Stock Entry")
|
||||
self.assertEqual(stock_entry.asset_repair, asset_repair.name)
|
||||
|
||||
def test_gl_entries_with_capitalized_asset_repair(self):
|
||||
asset = create_asset(is_existing_asset=1, calculate_depreciation=1, submit=1)
|
||||
asset_repair = create_asset_repair(
|
||||
asset=asset, capitalize_repair_cost=1, item="_Test Non Stock Item", submit=1
|
||||
)
|
||||
asset.reload()
|
||||
|
||||
GLEntry = qb.DocType("GL Entry")
|
||||
res = (
|
||||
qb.from_(GLEntry)
|
||||
.select(Sum(GLEntry.debit_in_account_currency).as_("total_debit"))
|
||||
.where(
|
||||
(GLEntry.voucher_type == "Asset Repair")
|
||||
& (GLEntry.voucher_no == asset_repair.name)
|
||||
& (GLEntry.against_voucher_type == "Asset")
|
||||
& (GLEntry.against_voucher == asset.name)
|
||||
& (GLEntry.company == asset.company)
|
||||
& (GLEntry.is_cancelled == 0)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
booked_value = res[0].total_debit if res else 0
|
||||
|
||||
self.assertEqual(asset.additional_asset_cost, asset_repair.repair_cost)
|
||||
self.assertEqual(booked_value, asset_repair.repair_cost)
|
||||
|
||||
|
||||
def num_of_depreciations(asset):
|
||||
return asset.finance_books[0].total_number_of_depreciations + (
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
"backflush_raw_materials_of_subcontract_based_on",
|
||||
"column_break_11",
|
||||
"over_transfer_allowance",
|
||||
"validate_consumed_qty",
|
||||
"section_break_xcug",
|
||||
"auto_create_subcontracting_order",
|
||||
"column_break_izrr",
|
||||
@@ -270,6 +271,14 @@
|
||||
"label": "Fixed Outgoing Email Account",
|
||||
"link_filters": "[[\"Email Account\",\"enable_outgoing\",\"=\",1]]",
|
||||
"options": "Email Account"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.backflush_raw_materials_of_subcontract_based_on == \"Material Transferred for Subcontract\"",
|
||||
"description": "Raw materials consumed qty will be validated based on FG BOM required qty",
|
||||
"fieldname": "validate_consumed_qty",
|
||||
"fieldtype": "Check",
|
||||
"label": "Validate Consumed Qty (as per BOM)"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -278,7 +287,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-20 22:13:38.506889",
|
||||
"modified": "2025-11-20 12:59:09.925862",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Buying Settings",
|
||||
|
||||
@@ -44,6 +44,7 @@ class BuyingSettings(Document):
|
||||
supp_master_name: DF.Literal["Supplier Name", "Naming Series", "Auto Name"]
|
||||
supplier_group: DF.Link | None
|
||||
use_transaction_date_exchange_rate: DF.Check
|
||||
validate_consumed_qty: DF.Check
|
||||
# end: auto-generated types
|
||||
|
||||
def validate(self):
|
||||
|
||||
@@ -317,6 +317,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
erpnext.buying.BuyingController
|
||||
) {
|
||||
setup() {
|
||||
this.setup_accounting_dimension_triggers();
|
||||
this.frm.custom_make_buttons = {
|
||||
"Purchase Receipt": "Purchase Receipt",
|
||||
"Purchase Invoice": "Purchase Invoice",
|
||||
|
||||
@@ -41,18 +41,20 @@ frappe.ui.form.on("Supplier", {
|
||||
|
||||
frm.set_query("supplier_primary_contact", function (doc) {
|
||||
return {
|
||||
query: "erpnext.buying.doctype.supplier.supplier.get_supplier_primary_contact",
|
||||
query: "erpnext.buying.doctype.supplier.supplier.get_supplier_primary",
|
||||
filters: {
|
||||
supplier: doc.name,
|
||||
type: "Contact",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("supplier_primary_address", function (doc) {
|
||||
return {
|
||||
query: "erpnext.buying.doctype.supplier.supplier.get_supplier_primary",
|
||||
filters: {
|
||||
link_doctype: "Supplier",
|
||||
link_name: doc.name,
|
||||
supplier: doc.name,
|
||||
type: "Address",
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -133,6 +135,14 @@ frappe.ui.form.on("Supplier", {
|
||||
// indicators
|
||||
erpnext.utils.set_party_dashboard_indicators(frm);
|
||||
}
|
||||
|
||||
frm.set_query("supplier_group", () => {
|
||||
return {
|
||||
filters: {
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
get_supplier_group_details: function (frm) {
|
||||
frappe.call({
|
||||
|
||||
@@ -219,19 +219,25 @@ class Supplier(TransactionBase):
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_supplier_primary_contact(doctype, txt, searchfield, start, page_len, filters):
|
||||
def get_supplier_primary(doctype, txt, searchfield, start, page_len, filters):
|
||||
supplier = filters.get("supplier")
|
||||
contact = frappe.qb.DocType("Contact")
|
||||
type = filters.get("type")
|
||||
type_doctype = frappe.qb.DocType(type)
|
||||
dynamic_link = frappe.qb.DocType("Dynamic Link")
|
||||
|
||||
return (
|
||||
frappe.qb.from_(contact)
|
||||
query = (
|
||||
frappe.qb.from_(type_doctype)
|
||||
.join(dynamic_link)
|
||||
.on(contact.name == dynamic_link.parent)
|
||||
.select(contact.name, contact.email_id)
|
||||
.on(type_doctype.name == dynamic_link.parent)
|
||||
.select(type_doctype.name)
|
||||
.where(
|
||||
(dynamic_link.link_name == supplier)
|
||||
& (dynamic_link.link_doctype == "Supplier")
|
||||
& (contact.name.like(f"%{txt}%"))
|
||||
& (type_doctype.name.like(f"%{txt}%"))
|
||||
)
|
||||
).run(as_dict=False)
|
||||
)
|
||||
|
||||
if type == "Contact":
|
||||
query = query.select(type_doctype.email_id)
|
||||
|
||||
return query.run()
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -7,6 +7,7 @@ from collections import defaultdict
|
||||
|
||||
import frappe
|
||||
from frappe import _, bold, qb, throw
|
||||
from frappe.contacts.doctype.address.address import get_address_display
|
||||
from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied
|
||||
from frappe.query_builder import Criterion, DocType
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
@@ -310,6 +311,31 @@ class AccountsController(TransactionBase):
|
||||
self.set_default_letter_head()
|
||||
self.validate_company_in_accounting_dimension()
|
||||
self.validate_party_address_and_contact()
|
||||
self.validate_company_linked_addresses()
|
||||
|
||||
def validate_company_linked_addresses(self):
|
||||
address_fields = []
|
||||
if self.doctype in ("Quotation", "Sales Order", "Delivery Note", "Sales Invoice"):
|
||||
address_fields = ["dispatch_address_name", "company_address"]
|
||||
elif self.doctype in ("Purchase Order", "Purchase Receipt", "Purchase Invoice", "Supplier Quotation"):
|
||||
address_fields = ["billing_address", "shipping_address"]
|
||||
|
||||
for field in address_fields:
|
||||
address = self.get(field)
|
||||
if address and not frappe.db.exists(
|
||||
"Dynamic Link",
|
||||
{
|
||||
"parent": address,
|
||||
"parenttype": "Address",
|
||||
"link_doctype": "Company",
|
||||
"link_name": self.company,
|
||||
},
|
||||
):
|
||||
frappe.throw(
|
||||
_("{0} does not belong to the {1}.").format(
|
||||
_(self.meta.get_label(field)), bold(self.company)
|
||||
)
|
||||
)
|
||||
|
||||
def set_default_letter_head(self):
|
||||
if hasattr(self, "letter_head") and not self.letter_head:
|
||||
@@ -364,6 +390,24 @@ class AccountsController(TransactionBase):
|
||||
|
||||
for _doctype in repost_doctypes:
|
||||
dt = frappe.qb.DocType(_doctype)
|
||||
|
||||
cancelled_entries = (
|
||||
frappe.qb.from_(dt)
|
||||
.select(dt.parent, dt.parenttype)
|
||||
.where((dt.voucher_type == self.doctype) & (dt.voucher_no == self.name) & (dt.docstatus == 2))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
if cancelled_entries:
|
||||
entries = "<br>".join([get_link_to_form(d.parenttype, d.parent) for d in cancelled_entries])
|
||||
|
||||
frappe.throw(
|
||||
_(
|
||||
"The following cancelled repost entries exist for <b>{0}</b>:<br><br>{1}<br><br>"
|
||||
"Kindly delete these entries before continuing."
|
||||
).format(self.name, entries)
|
||||
)
|
||||
|
||||
rows = (
|
||||
frappe.qb.from_(dt)
|
||||
.select(dt.name, dt.parent, dt.parenttype)
|
||||
@@ -2313,7 +2357,7 @@ class AccountsController(TransactionBase):
|
||||
|
||||
def validate_party(self):
|
||||
party_type, party = self.get_party()
|
||||
validate_party_frozen_disabled(party_type, party)
|
||||
validate_party_frozen_disabled(self.company, party_type, party)
|
||||
|
||||
def get_party(self):
|
||||
party_type = None
|
||||
@@ -4167,3 +4211,130 @@ def update_gl_dict_with_regional_fields(doc, gl_dict):
|
||||
def update_gl_dict_with_app_based_fields(doc, gl_dict):
|
||||
for method in frappe.get_hooks("update_gl_dict_with_app_based_fields", default=[]):
|
||||
frappe.get_attr(method)(doc, gl_dict)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_missing_company_details(doctype, docname):
|
||||
from frappe.contacts.doctype.address.address import get_address_display_list
|
||||
|
||||
company = frappe.db.get_value(doctype, docname, "company")
|
||||
if doctype == "Purchase Order":
|
||||
company_address = frappe.db.get_value(doctype, docname, "billing_address")
|
||||
else:
|
||||
company_address = frappe.db.get_value(doctype, docname, "company_address")
|
||||
|
||||
company_details = frappe.get_value(
|
||||
"Company", company, ["company_logo", "website", "phone_no", "email"], as_dict=True
|
||||
)
|
||||
|
||||
required_fields = [
|
||||
company_details.get("company_logo"),
|
||||
company_details.get("phone_no"),
|
||||
company_details.get("email"),
|
||||
]
|
||||
|
||||
if not all(required_fields) and not frappe.has_permission("Company", "write", throw=False):
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Some required Company details are missing. You don't have permission to update them. Please contact your System Manager."
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if not company_address and not frappe.has_permission(doctype, "write", throw=False):
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Company Address is missing. You don't have permission to update it. Please contact your System Manager."
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
address_display_list = get_address_display_list("Company", company)
|
||||
address_line = address_display_list[0].get("address_line1") if address_display_list else ""
|
||||
|
||||
required_fields.append(company_address)
|
||||
required_fields.append(address_line)
|
||||
|
||||
if all(required_fields):
|
||||
return False
|
||||
return {
|
||||
"company_logo": company_details.get("company_logo"),
|
||||
"website": company_details.get("website"),
|
||||
"phone_no": company_details.get("phone_no"),
|
||||
"email": company_details.get("email"),
|
||||
"address_line": address_line,
|
||||
"company": company,
|
||||
"company_address": company_address,
|
||||
"name": docname,
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_company_master_and_address(current_doctype, name, company, details):
|
||||
from frappe.utils import validate_email_address
|
||||
|
||||
if isinstance(details, str):
|
||||
details = frappe.parse_json(details)
|
||||
|
||||
if details.get("email"):
|
||||
validate_email_address(details.get("email"), throw=True)
|
||||
|
||||
company_fields = ["company_logo", "website", "phone_no", "email"]
|
||||
company_fields_to_update = {field: details.get(field) for field in company_fields if details.get(field)}
|
||||
|
||||
if company_fields_to_update:
|
||||
frappe.db.set_value("Company", company, company_fields_to_update)
|
||||
|
||||
company_address = details.get("company_address")
|
||||
if details.get("address_line1"):
|
||||
address_doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Address",
|
||||
"address_title": details.get("address_title"),
|
||||
"address_type": details.get("address_type"),
|
||||
"address_line1": details.get("address_line1"),
|
||||
"address_line2": details.get("address_line2"),
|
||||
"city": details.get("city"),
|
||||
"state": details.get("state"),
|
||||
"pincode": details.get("pincode"),
|
||||
"country": details.get("country"),
|
||||
"is_your_company_address": 1,
|
||||
"links": [{"link_doctype": "Company", "link_name": company}],
|
||||
}
|
||||
)
|
||||
address_doc.insert()
|
||||
company_address = address_doc.name
|
||||
|
||||
update_doc_company_address(current_doctype, name, company_address, details)
|
||||
|
||||
|
||||
def update_doc_company_address(current_doctype, docname, company_address, details):
|
||||
if not company_address:
|
||||
return
|
||||
|
||||
address_field_map = {
|
||||
"Purchase Order": ("billing_address", "billing_address_display"),
|
||||
"Sales Invoice": ("company_address", "company_address_display"),
|
||||
"Delivery Note": ("company_address", "company_address_display"),
|
||||
"POS Invoice": ("company_address", "company_address_display"),
|
||||
}
|
||||
|
||||
address_field, display_field = address_field_map.get(
|
||||
current_doctype, ("company_address", "company_address_display")
|
||||
)
|
||||
|
||||
current_display = frappe.db.get_value(current_doctype, docname, display_field)
|
||||
|
||||
if current_display and not details.get("address_line1"):
|
||||
return
|
||||
|
||||
from frappe.query_builder import DocType
|
||||
|
||||
DocType = DocType(current_doctype)
|
||||
|
||||
(
|
||||
frappe.qb.update(DocType)
|
||||
.set(getattr(DocType, address_field), company_address)
|
||||
.set(getattr(DocType, display_field), get_address_display(company_address))
|
||||
.where(DocType.name == docname)
|
||||
).run()
|
||||
|
||||
@@ -186,7 +186,7 @@ def validate_quantity(doc, key, args, ref, valid_items, already_returned_items):
|
||||
frappe.get_meta(doc.doctype + " Item").get_field(
|
||||
"stock_qty" if doc.get("update_stock", "") else "qty"
|
||||
),
|
||||
company_currency,
|
||||
currency=company_currency,
|
||||
)
|
||||
|
||||
for column in fields:
|
||||
|
||||
@@ -25,6 +25,7 @@ from erpnext.controllers.sales_and_purchase_return import (
|
||||
from erpnext.setup.doctype.brand.brand import get_brand_defaults
|
||||
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
|
||||
from erpnext.stock import get_warehouse_account_map
|
||||
from erpnext.stock.doctype.batch.batch import get_batch_qty
|
||||
from erpnext.stock.doctype.inventory_dimension.inventory_dimension import (
|
||||
get_evaluated_inventory_dimension,
|
||||
)
|
||||
@@ -1216,6 +1217,12 @@ class StockController(AccountsController):
|
||||
],
|
||||
}.get(self.doctype)
|
||||
|
||||
qty_field = {
|
||||
"Sales Invoice": "qty",
|
||||
"Delivery Note": "qty",
|
||||
"Stock Entry": "fg_completed_qty",
|
||||
}.get(self.doctype)
|
||||
|
||||
reserved_batches_data = self.get_reserved_batches(batches)
|
||||
items = self.items
|
||||
if self.doctype == "Stock Entry":
|
||||
@@ -1236,6 +1243,17 @@ class StockController(AccountsController):
|
||||
if row.voucher_no == value:
|
||||
continue
|
||||
|
||||
batch_qty = get_batch_qty(
|
||||
row.batch_no,
|
||||
row.warehouse,
|
||||
posting_date=self.posting_date,
|
||||
posting_time=self.posting_time,
|
||||
consider_negative_batches=True,
|
||||
)
|
||||
|
||||
if item.get(qty_field) < batch_qty:
|
||||
continue
|
||||
|
||||
frappe.throw(
|
||||
_(
|
||||
"The batch {0} is already reserved in {1} {2}. So, cannot proceed with the {3} {4}, which is created against the {5} {6}."
|
||||
@@ -1264,6 +1282,7 @@ class StockController(AccountsController):
|
||||
doctype.voucher_type,
|
||||
doctype.voucher_no,
|
||||
doctype.item_code,
|
||||
doctype.warehouse,
|
||||
)
|
||||
.where((doctype.docstatus == 1) & (child_doc.batch_no.isin(batches)))
|
||||
).run(as_dict=True)
|
||||
@@ -1626,7 +1645,7 @@ class StockController(AccountsController):
|
||||
rule = frappe.db.get_value(
|
||||
"Putaway Rule",
|
||||
{"item_code": item.get("item_code"), "warehouse": item.get(warehouse_field)},
|
||||
["name", "disable"],
|
||||
["stock_capacity", "name", "disable"],
|
||||
as_dict=True,
|
||||
)
|
||||
if rule:
|
||||
@@ -1645,7 +1664,11 @@ class StockController(AccountsController):
|
||||
rule_map[rule_name]["warehouse"] = item.get(warehouse_field)
|
||||
rule_map[rule_name]["item"] = item.get("item_code")
|
||||
rule_map[rule_name]["qty_put"] = 0
|
||||
rule_map[rule_name]["capacity"] = get_available_putaway_capacity(rule_name)
|
||||
rule_map[rule_name]["capacity"] = (
|
||||
rule.stock_capacity
|
||||
if self.doctype == "Stock Reconciliation"
|
||||
else get_available_putaway_capacity(rule_name)
|
||||
)
|
||||
rule_map[rule_name]["qty_put"] += flt(stock_qty)
|
||||
|
||||
for rule, values in rule_map.items():
|
||||
|
||||
@@ -11,6 +11,7 @@ from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.utils import cint, flt, get_link_to_form
|
||||
|
||||
from erpnext.controllers.stock_controller import StockController
|
||||
from erpnext.stock.doctype.batch.batch import get_batch_qty
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.serial_and_batch_bundle import (
|
||||
combine_datetime,
|
||||
get_auto_batch_nos,
|
||||
@@ -549,7 +550,7 @@ class SubcontractingController(StockController):
|
||||
if item.get("serial_and_batch_bundle"):
|
||||
frappe.delete_doc("Serial and Batch Bundle", item.serial_and_batch_bundle, force=True)
|
||||
|
||||
def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
|
||||
def _get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
|
||||
data = []
|
||||
|
||||
doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"
|
||||
@@ -590,7 +591,7 @@ class SubcontractingController(StockController):
|
||||
to_remove = []
|
||||
for item in data:
|
||||
if item.is_phantom_item:
|
||||
data += self.__get_materials_from_bom(
|
||||
data += self._get_materials_from_bom(
|
||||
item.rm_item_code, item.bom_no, exploded_item=exploded_item
|
||||
)
|
||||
to_remove.append(item)
|
||||
@@ -758,7 +759,11 @@ class SubcontractingController(StockController):
|
||||
serial_nos = get_filtered_serial_nos(serial_nos, self, "supplied_items")
|
||||
row.serial_no = "\n".join(serial_nos)
|
||||
|
||||
elif item_details.has_batch_no and not row.serial_and_batch_bundle and not row.batch_no:
|
||||
elif (
|
||||
item_details.has_batch_no
|
||||
and not row.serial_and_batch_bundle
|
||||
and (not row.batch_no or self.batch_has_not_available(row.batch_no, row.consumed_qty))
|
||||
):
|
||||
batches = get_auto_batch_nos(kwargs)
|
||||
if batches:
|
||||
consumed_qty = row.consumed_qty
|
||||
@@ -783,6 +788,11 @@ class SubcontractingController(StockController):
|
||||
)
|
||||
consumed_qty -= d.get("qty")
|
||||
|
||||
def batch_has_not_available(self, batch_no, qty_required):
|
||||
batch_qty = get_batch_qty(batch_no, self.supplier_warehouse, consider_negative_batches=True)
|
||||
|
||||
return batch_qty < qty_required
|
||||
|
||||
def update_rate_for_supplied_items(self):
|
||||
if self.doctype != "Subcontracting Receipt":
|
||||
return
|
||||
@@ -921,7 +931,7 @@ class SubcontractingController(StockController):
|
||||
if self.doctype == self.subcontract_data.order_doctype or (
|
||||
self.backflush_based_on == "BOM" or self.is_return
|
||||
):
|
||||
for bom_item in self.__get_materials_from_bom(
|
||||
for bom_item in self._get_materials_from_bom(
|
||||
row.item_code, row.bom, row.get("include_exploded_items")
|
||||
):
|
||||
qty = flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor
|
||||
|
||||
@@ -16,7 +16,10 @@ from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_pay
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import prepare_data_for_internal_transfer
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import (
|
||||
create_purchase_order,
|
||||
prepare_data_for_internal_transfer,
|
||||
)
|
||||
from erpnext.projects.doctype.project.test_project import make_project
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
|
||||
@@ -2432,3 +2435,34 @@ class TestAccountsController(IntegrationTestCase):
|
||||
|
||||
# Second return should only get remaining discount (100 - 60 = 40)
|
||||
self.assertEqual(return_si_2.discount_amount, -40)
|
||||
|
||||
def test_company_linked_address(self):
|
||||
from erpnext.crm.doctype.prospect.test_prospect import make_address
|
||||
|
||||
company_address = make_address(
|
||||
address_title="Company", address_type="Shipping", address_line1="100", city="Mumbai"
|
||||
)
|
||||
company_address.append("links", {"link_doctype": "Company", "link_name": "_Test Company"})
|
||||
company_address.save()
|
||||
|
||||
customer_shipping = make_address(
|
||||
address_title="Customer", address_type="Shipping", address_line1="10"
|
||||
)
|
||||
customer_shipping.append("links", {"link_doctype": "Customer", "link_name": "_Test Customer"})
|
||||
customer_shipping.save()
|
||||
|
||||
supplier_billing = make_address(address_title="Supplier", address_line1="2", city="Ahmedabad")
|
||||
supplier_billing.append("links", {"link_doctype": "Supplier", "link_name": "_Test Supplier"})
|
||||
supplier_billing.save()
|
||||
|
||||
po = create_purchase_order(do_not_save=True)
|
||||
po.shipping_address = customer_shipping.name
|
||||
self.assertRaises(frappe.ValidationError, po.save)
|
||||
po.shipping_address = company_address.name
|
||||
po.save()
|
||||
|
||||
po.billing_address = supplier_billing.name
|
||||
self.assertRaises(frappe.ValidationError, po.save)
|
||||
po.billing_address = company_address.name
|
||||
po.reload()
|
||||
po.save()
|
||||
|
||||
@@ -123,7 +123,7 @@ def send_mail(entry, email_campaign):
|
||||
subject=frappe.render_template(email_template.get("subject"), context),
|
||||
content=frappe.render_template(email_template.response_, context),
|
||||
sender=sender,
|
||||
recipients=recipient_list,
|
||||
bcc=recipient_list,
|
||||
communication_medium="Email",
|
||||
sent_or_received="Sent",
|
||||
send_email=True,
|
||||
|
||||
3674
erpnext/locale/ar.po
3674
erpnext/locale/ar.po
File diff suppressed because it is too large
Load Diff
3691
erpnext/locale/bs.po
3691
erpnext/locale/bs.po
File diff suppressed because it is too large
Load Diff
3670
erpnext/locale/cs.po
3670
erpnext/locale/cs.po
File diff suppressed because it is too large
Load Diff
3672
erpnext/locale/da.po
3672
erpnext/locale/da.po
File diff suppressed because it is too large
Load Diff
3678
erpnext/locale/de.po
3678
erpnext/locale/de.po
File diff suppressed because it is too large
Load Diff
3678
erpnext/locale/eo.po
3678
erpnext/locale/eo.po
File diff suppressed because it is too large
Load Diff
3676
erpnext/locale/es.po
3676
erpnext/locale/es.po
File diff suppressed because it is too large
Load Diff
3978
erpnext/locale/fa.po
3978
erpnext/locale/fa.po
File diff suppressed because it is too large
Load Diff
3676
erpnext/locale/fr.po
3676
erpnext/locale/fr.po
File diff suppressed because it is too large
Load Diff
3691
erpnext/locale/hr.po
3691
erpnext/locale/hr.po
File diff suppressed because it is too large
Load Diff
3670
erpnext/locale/hu.po
3670
erpnext/locale/hu.po
File diff suppressed because it is too large
Load Diff
3676
erpnext/locale/id.po
3676
erpnext/locale/id.po
File diff suppressed because it is too large
Load Diff
3670
erpnext/locale/it.po
3670
erpnext/locale/it.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
3670
erpnext/locale/my.po
3670
erpnext/locale/my.po
File diff suppressed because it is too large
Load Diff
3670
erpnext/locale/nb.po
3670
erpnext/locale/nb.po
File diff suppressed because it is too large
Load Diff
3670
erpnext/locale/nl.po
3670
erpnext/locale/nl.po
File diff suppressed because it is too large
Load Diff
3670
erpnext/locale/pl.po
3670
erpnext/locale/pl.po
File diff suppressed because it is too large
Load Diff
3670
erpnext/locale/pt.po
3670
erpnext/locale/pt.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
3670
erpnext/locale/ru.po
3670
erpnext/locale/ru.po
File diff suppressed because it is too large
Load Diff
6068
erpnext/locale/sl.po
6068
erpnext/locale/sl.po
File diff suppressed because it is too large
Load Diff
3861
erpnext/locale/sr.po
3861
erpnext/locale/sr.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
3705
erpnext/locale/sv.po
3705
erpnext/locale/sv.po
File diff suppressed because it is too large
Load Diff
3670
erpnext/locale/ta.po
3670
erpnext/locale/ta.po
File diff suppressed because it is too large
Load Diff
3672
erpnext/locale/th.po
3672
erpnext/locale/th.po
File diff suppressed because it is too large
Load Diff
3678
erpnext/locale/tr.po
3678
erpnext/locale/tr.po
File diff suppressed because it is too large
Load Diff
3670
erpnext/locale/vi.po
3670
erpnext/locale/vi.po
File diff suppressed because it is too large
Load Diff
3678
erpnext/locale/zh.po
3678
erpnext/locale/zh.po
File diff suppressed because it is too large
Load Diff
@@ -460,10 +460,12 @@ frappe.ui.form.on("BOM", {
|
||||
);
|
||||
|
||||
has_template_rm.forEach((d) => {
|
||||
let bom_qty = dialog.fields_dict.qty?.value || 1;
|
||||
|
||||
dialog.fields_dict.items.df.data.push({
|
||||
item_code: d.item_code,
|
||||
variant_item_code: "",
|
||||
qty: (d.qty / frm.doc.quantity) * (dialog.fields_dict.qty.value || 1),
|
||||
qty: flt(d.qty / frm.doc.quantity) * flt(bom_qty),
|
||||
source_warehouse: d.source_warehouse,
|
||||
operation: d.operation,
|
||||
});
|
||||
|
||||
@@ -916,7 +916,9 @@ class BOM(WebsiteGenerator):
|
||||
)
|
||||
|
||||
d.base_rate = flt(d.rate) * flt(self.conversion_rate)
|
||||
d.amount = flt(d.rate, d.precision("rate")) * flt(d.qty, d.precision("qty"))
|
||||
d.amount = flt(
|
||||
flt(d.rate, d.precision("rate")) * flt(d.qty, d.precision("qty")), d.precision("amount")
|
||||
)
|
||||
d.base_amount = d.amount * flt(self.conversion_rate)
|
||||
d.qty_consumed_per_unit = flt(d.stock_qty, d.precision("stock_qty")) / flt(
|
||||
self.quantity, self.precision("quantity")
|
||||
@@ -939,7 +941,10 @@ class BOM(WebsiteGenerator):
|
||||
d.base_rate = flt(d.rate, d.precision("rate")) * flt(
|
||||
self.conversion_rate, self.precision("conversion_rate")
|
||||
)
|
||||
d.amount = flt(d.rate, d.precision("rate")) * flt(d.stock_qty, d.precision("stock_qty"))
|
||||
d.amount = flt(
|
||||
flt(d.rate, d.precision("rate")) * flt(d.stock_qty, d.precision("stock_qty")),
|
||||
d.precision("amount"),
|
||||
)
|
||||
d.base_amount = flt(d.amount, d.precision("amount")) * flt(
|
||||
self.conversion_rate, self.precision("conversion_rate")
|
||||
)
|
||||
@@ -1408,7 +1413,7 @@ def validate_bom_no(item, bom_no):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_children(parent=None, return_all=True, fetch_phantom_items=False, is_root=False, **filters):
|
||||
def get_children(parent=None, is_root=False, **filters):
|
||||
if not parent or parent == "BOM":
|
||||
frappe.msgprint(_("Please select a BOM"))
|
||||
return
|
||||
@@ -1420,13 +1425,10 @@ def get_children(parent=None, return_all=True, fetch_phantom_items=False, is_roo
|
||||
bom_doc = frappe.get_cached_doc("BOM", frappe.form_dict.parent)
|
||||
frappe.has_permission("BOM", doc=bom_doc, throw=True)
|
||||
|
||||
filters = [["parent", "=", frappe.form_dict.parent]]
|
||||
if not return_all:
|
||||
filters.append(["is_phantom_item", "=", cint(fetch_phantom_items)])
|
||||
bom_items = frappe.get_all(
|
||||
"BOM Item",
|
||||
fields=["item_code", "bom_no as value", "stock_qty", "qty", "is_phantom_item", "bom_no"],
|
||||
filters=filters,
|
||||
filters=[["parent", "=", frappe.form_dict.parent]],
|
||||
order_by="idx",
|
||||
)
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, flt, sbool
|
||||
from pypika.terms import ValueWrapper
|
||||
|
||||
from erpnext.manufacturing.doctype.bom.bom import get_bom_item_rate
|
||||
|
||||
@@ -373,7 +374,7 @@ def get_children(doctype=None, parent=None, **kwargs):
|
||||
"parent as parent_id",
|
||||
"qty",
|
||||
"idx",
|
||||
"'BOM Creator Item' as doctype",
|
||||
ValueWrapper("BOM Creator Item").as_("doctype"),
|
||||
"name",
|
||||
"uom",
|
||||
"rate",
|
||||
|
||||
@@ -221,6 +221,9 @@ frappe.ui.form.on("Job Card", {
|
||||
label: __("Select Employees"),
|
||||
options: "Job Card Time Log",
|
||||
fieldname: "employees",
|
||||
filters: {
|
||||
status: "Active",
|
||||
},
|
||||
},
|
||||
(d) => {
|
||||
frm.events.start_timer(frm, from_time, d.employees);
|
||||
|
||||
@@ -220,17 +220,12 @@ class JobCard(Document):
|
||||
|
||||
if job_card_qty and ((job_card_qty - completed_qty) > wo_qty):
|
||||
form_link = get_link_to_form("Manufacturing Settings", "Manufacturing Settings")
|
||||
|
||||
msg = f"""
|
||||
Qty To Manufacture in the job card
|
||||
cannot be greater than Qty To Manufacture in the
|
||||
work order for the operation {bold(self.operation)}.
|
||||
<br><br><b>Solution: </b> Either you can reduce the
|
||||
Qty To Manufacture in the job card or set the
|
||||
'Overproduction Percentage For Work Order'
|
||||
in the {form_link}."""
|
||||
|
||||
frappe.throw(_(msg), title=_("Extra Job Card Quantity"))
|
||||
frappe.throw(
|
||||
_(
|
||||
"Qty To Manufacture in the job card cannot be greater than Qty To Manufacture in the work order for the operation {0}. <br><br><b>Solution: </b> Either you can reduce the Qty To Manufacture in the job card or set the 'Overproduction Percentage For Work Order' in the {1}."
|
||||
).format(bold(self.operation), form_link),
|
||||
title=_("Extra Job Card Quantity"),
|
||||
)
|
||||
|
||||
def set_sub_operations(self):
|
||||
if not self.sub_operations and self.operation:
|
||||
@@ -945,6 +940,36 @@ class JobCard(Document):
|
||||
},
|
||||
)
|
||||
|
||||
def set_consumed_qty_in_job_card_item(self, ste_doc):
|
||||
jc_item_names = [row.job_card_item for row in ste_doc.get("items") if row.get("job_card_item")]
|
||||
|
||||
if not jc_item_names:
|
||||
return
|
||||
|
||||
se = frappe.qb.DocType("Stock Entry")
|
||||
sed = frappe.qb.DocType("Stock Entry Detail")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(sed)
|
||||
.join(se)
|
||||
.on(sed.parent == se.name)
|
||||
.select(sed.job_card_item, Sum(sed.qty))
|
||||
.where(
|
||||
(sed.job_card_item.isin(jc_item_names)) & (se.docstatus == 1) & (se.purpose == "Manufacture")
|
||||
)
|
||||
.groupby(sed.job_card_item)
|
||||
)
|
||||
|
||||
itemwise_consumed_qty = frappe._dict(query.run(as_list=True))
|
||||
|
||||
for row in ste_doc.items:
|
||||
if not row.get("job_card_item"):
|
||||
continue
|
||||
|
||||
consumed_qty = flt(itemwise_consumed_qty.get(row.job_card_item, 0.0))
|
||||
|
||||
frappe.db.set_value("Job Card Item", row.job_card_item, "consumed_qty", consumed_qty)
|
||||
|
||||
def set_transferred_qty_in_job_card_item(self, ste_doc):
|
||||
def _get_job_card_items_transferred_qty(ste_doc):
|
||||
from frappe.query_builder.functions import Sum
|
||||
@@ -1170,14 +1195,16 @@ class JobCard(Document):
|
||||
)
|
||||
|
||||
if row.completed_qty < current_operation_qty:
|
||||
msg = f"""The completed quantity {bold(current_operation_qty)}
|
||||
of an operation {bold(self.operation)} cannot be greater
|
||||
than the completed quantity {bold(row.completed_qty)}
|
||||
of a previous operation
|
||||
{bold(row.operation)}.
|
||||
"""
|
||||
|
||||
frappe.throw(_(msg))
|
||||
frappe.throw(
|
||||
_(
|
||||
"The completed quantity {0} of an operation {1} cannot be greater than the completed quantity {2} of a previous operation {3}."
|
||||
).format(
|
||||
bold(current_operation_qty),
|
||||
bold(self.operation),
|
||||
bold(row.completed_qty),
|
||||
bold(row.operation),
|
||||
)
|
||||
)
|
||||
|
||||
def validate_work_order(self):
|
||||
if self.is_work_order_closed():
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"required_qty",
|
||||
"column_break_9",
|
||||
"transferred_qty",
|
||||
"consumed_qty",
|
||||
"allow_alternative_item"
|
||||
],
|
||||
"fields": [
|
||||
@@ -100,20 +101,28 @@
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "consumed_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Consumed Qty",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:09:56.943741",
|
||||
"modified": "2025-12-04 14:30:19.472294",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Job Card Item",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ class JobCardItem(Document):
|
||||
from frappe.types import DF
|
||||
|
||||
allow_alternative_item: DF.Check
|
||||
consumed_qty: DF.Float
|
||||
description: DF.Text | None
|
||||
item_code: DF.Link | None
|
||||
item_group: DF.Link | None
|
||||
|
||||
@@ -1661,23 +1661,6 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
|
||||
] += d.get("qty")
|
||||
sub_assembly_items = {k[:2]: v for k, v in sub_assembly_items.items()}
|
||||
|
||||
data = []
|
||||
for row in doc.get("po_items"):
|
||||
get_sub_assembly_items(
|
||||
[],
|
||||
frappe._dict(),
|
||||
row.get("bom_no"),
|
||||
data,
|
||||
row.get("planned_qty"),
|
||||
doc.get("company"),
|
||||
warehouse=doc.get("sub_assembly_warehouse"),
|
||||
skip_available_sub_assembly_item=doc.get("skip_available_sub_assembly_item"),
|
||||
fetch_phantom_items=True,
|
||||
)
|
||||
|
||||
for d in data:
|
||||
sub_assembly_items[(d.get("production_item"), d.get("bom_no"))] += d.get("stock_qty")
|
||||
|
||||
for data in po_items:
|
||||
if not data.get("include_exploded_items") and doc.get("sub_assembly_items"):
|
||||
data["include_exploded_items"] = 1
|
||||
@@ -1896,9 +1879,8 @@ def get_sub_assembly_items(
|
||||
warehouse=None,
|
||||
indent=0,
|
||||
skip_available_sub_assembly_item=False,
|
||||
fetch_phantom_items=False,
|
||||
):
|
||||
data = get_bom_children(parent=bom_no, return_all=False, fetch_phantom_items=fetch_phantom_items)
|
||||
data = get_bom_children(parent=bom_no)
|
||||
for d in data:
|
||||
if d.expandable:
|
||||
parent_item_code = frappe.get_cached_value("BOM", bom_no, "item")
|
||||
@@ -1921,31 +1903,32 @@ def get_sub_assembly_items(
|
||||
elif warehouse:
|
||||
bin_details.setdefault(d.item_code, get_bin_details(d, company, for_warehouse=warehouse))
|
||||
|
||||
bom_data.append(
|
||||
frappe._dict(
|
||||
{
|
||||
"actual_qty": bin_details[d.item_code][0].get("actual_qty", 0)
|
||||
if bin_details.get(d.item_code)
|
||||
else 0,
|
||||
"parent_item_code": parent_item_code,
|
||||
"description": d.description,
|
||||
"production_item": d.item_code,
|
||||
"item_name": d.item_name,
|
||||
"stock_uom": d.stock_uom,
|
||||
"uom": d.stock_uom,
|
||||
"bom_no": d.value,
|
||||
"is_sub_contracted_item": d.is_sub_contracted_item,
|
||||
"bom_level": indent,
|
||||
"indent": indent,
|
||||
"stock_qty": stock_qty,
|
||||
"required_qty": required_qty,
|
||||
"projected_qty": bin_details[d.item_code][0].get("projected_qty", 0)
|
||||
if bin_details.get(d.item_code)
|
||||
else 0,
|
||||
"main_bom": bom_no,
|
||||
}
|
||||
if not d.is_phantom_item:
|
||||
bom_data.append(
|
||||
frappe._dict(
|
||||
{
|
||||
"actual_qty": bin_details[d.item_code][0].get("actual_qty", 0)
|
||||
if bin_details.get(d.item_code)
|
||||
else 0,
|
||||
"parent_item_code": parent_item_code,
|
||||
"description": d.description,
|
||||
"production_item": d.item_code,
|
||||
"item_name": d.item_name,
|
||||
"stock_uom": d.stock_uom,
|
||||
"uom": d.stock_uom,
|
||||
"bom_no": d.value,
|
||||
"is_sub_contracted_item": d.is_sub_contracted_item,
|
||||
"bom_level": indent,
|
||||
"indent": indent,
|
||||
"stock_qty": stock_qty,
|
||||
"required_qty": required_qty,
|
||||
"projected_qty": bin_details[d.item_code][0].get("projected_qty", 0)
|
||||
if bin_details.get(d.item_code)
|
||||
else 0,
|
||||
"main_bom": bom_no,
|
||||
}
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if d.value:
|
||||
get_sub_assembly_items(
|
||||
@@ -1958,7 +1941,6 @@ def get_sub_assembly_items(
|
||||
warehouse,
|
||||
indent=indent + 1,
|
||||
skip_available_sub_assembly_item=skip_available_sub_assembly_item,
|
||||
fetch_phantom_items=fetch_phantom_items,
|
||||
)
|
||||
|
||||
|
||||
@@ -2016,22 +1998,14 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
|
||||
return reserved_qty_for_production_plan - reserved_qty_for_production
|
||||
|
||||
|
||||
@frappe.request_cache
|
||||
def get_non_completed_production_plans():
|
||||
table = frappe.qb.DocType("Production Plan")
|
||||
child = frappe.qb.DocType("Production Plan Item")
|
||||
|
||||
return (
|
||||
frappe.qb.from_(table)
|
||||
.inner_join(child)
|
||||
.on(table.name == child.parent)
|
||||
.select(table.name)
|
||||
.distinct()
|
||||
.where(
|
||||
(table.docstatus == 1)
|
||||
& (table.status.notin(["Completed", "Closed"]))
|
||||
& (child.planned_qty > child.ordered_qty)
|
||||
)
|
||||
.where((table.docstatus == 1) & (table.status.notin(["Completed", "Closed"])))
|
||||
).run(pluck="name")
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user