mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-10 08:23:01 +00:00
Compare commits
164 Commits
v15.108.3
...
version-15
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b3e3dfd83 | ||
|
|
316bb13853 | ||
|
|
18ca96c36b | ||
|
|
95f46dfc01 | ||
|
|
0e64acb0fa | ||
|
|
a5c23a3d16 | ||
|
|
2f5b93e308 | ||
|
|
aeed6a459f | ||
|
|
c0f56cd284 | ||
|
|
a94e362b8c | ||
|
|
a121048f9b | ||
|
|
cedd0a1903 | ||
|
|
8bccc25b1a | ||
|
|
335173c5ba | ||
|
|
ba19a24526 | ||
|
|
2c4b89d1df | ||
|
|
14bafe9dfd | ||
|
|
77d9849d16 | ||
|
|
09453f883b | ||
|
|
dc1fb3f804 | ||
|
|
07b61113af | ||
|
|
2d1c0dcb53 | ||
|
|
45b232d369 | ||
|
|
7852ea65af | ||
|
|
999cfef619 | ||
|
|
2d0e3fd9af | ||
|
|
4c9c2911a6 | ||
|
|
e0a0b3eafc | ||
|
|
8c9168595f | ||
|
|
e7eaa87a77 | ||
|
|
ffb36cff78 | ||
|
|
4fadc4aab6 | ||
|
|
bde46cffd0 | ||
|
|
08f3cf98f9 | ||
|
|
55b0715310 | ||
|
|
c15012cd51 | ||
|
|
e8e0514a30 | ||
|
|
ddaf75a60d | ||
|
|
013bd1a566 | ||
|
|
e8267e3237 | ||
|
|
d5477b096d | ||
|
|
6d3f9d3c6f | ||
|
|
f65b56d73f | ||
|
|
271ddb6add | ||
|
|
9095c5a3c2 | ||
|
|
0a7c3581da | ||
|
|
611849f953 | ||
|
|
5a80de43fa | ||
|
|
4772799db2 | ||
|
|
ccbca57420 | ||
|
|
d6b2fb2f96 | ||
|
|
a6b7142c18 | ||
|
|
6796617921 | ||
|
|
c65d768020 | ||
|
|
7200c22890 | ||
|
|
aa94c3ff22 | ||
|
|
87c6ad4f85 | ||
|
|
35b4ada3e2 | ||
|
|
ad511b80c0 | ||
|
|
ad55c7c372 | ||
|
|
689a3f50ae | ||
|
|
96bd97dd6d | ||
|
|
897722c35f | ||
|
|
54cbc91166 | ||
|
|
ecf9aa146c | ||
|
|
42e2fd5fc9 | ||
|
|
10664b7b95 | ||
|
|
1980307048 | ||
|
|
13eeddd1f6 | ||
|
|
dc08b615f1 | ||
|
|
ce94f4fd11 | ||
|
|
e314d0cfc5 | ||
|
|
741216d3eb | ||
|
|
94e15ae9ef | ||
|
|
4a6af25d11 | ||
|
|
c7fbc133e6 | ||
|
|
e5aa45cf0d | ||
|
|
3e3689d938 | ||
|
|
d0fc3f029f | ||
|
|
a9cfa22199 | ||
|
|
1238aeb30a | ||
|
|
eebb37f9fd | ||
|
|
f8a123e79d | ||
|
|
1c5220b86f | ||
|
|
779f1b6104 | ||
|
|
c2063c4707 | ||
|
|
e429e608c2 | ||
|
|
75d00ef173 | ||
|
|
94fd15e550 | ||
|
|
03532624b8 | ||
|
|
338feb31e1 | ||
|
|
2a805e090c | ||
|
|
2a12ae1afe | ||
|
|
715ca39abc | ||
|
|
cad14ac3e6 | ||
|
|
2a52ea6850 | ||
|
|
a2d924c48f | ||
|
|
3ad39c987b | ||
|
|
5c392d6123 | ||
|
|
9e7b03173d | ||
|
|
bfdf1e43f9 | ||
|
|
ad6e3a45d2 | ||
|
|
4669ff295f | ||
|
|
264433b23d | ||
|
|
41bf2f32fd | ||
|
|
a6d4bc5c86 | ||
|
|
93dcba40ec | ||
|
|
067c23f20e | ||
|
|
16fbf8299f | ||
|
|
7ce7e3d5e5 | ||
|
|
60fdc6bc1a | ||
|
|
b972b7c307 | ||
|
|
4927d346c8 | ||
|
|
8f164cff1d | ||
|
|
31c251d956 | ||
|
|
bc81992a40 | ||
|
|
66267cf99a | ||
|
|
9d211990c3 | ||
|
|
76078a7fb9 | ||
|
|
cba4c9f0ee | ||
|
|
46d5395148 | ||
|
|
b8b2141e20 | ||
|
|
4436585aa0 | ||
|
|
937eb87932 | ||
|
|
6a21d28030 | ||
|
|
4f89f3a856 | ||
|
|
8b241b45e2 | ||
|
|
b517f26085 | ||
|
|
f28b948e1b | ||
|
|
a797ab3482 | ||
|
|
d31a051c74 | ||
|
|
aad270914a | ||
|
|
af3e7f53ac | ||
|
|
238f1685f1 | ||
|
|
418a7fb301 | ||
|
|
304474d2f7 | ||
|
|
59e9f5192c | ||
|
|
914576040e | ||
|
|
ff442cd8e7 | ||
|
|
25739ae217 | ||
|
|
425e6c52f4 | ||
|
|
97d2152a36 | ||
|
|
034e159ee4 | ||
|
|
fff023bf7b | ||
|
|
429e02e6f9 | ||
|
|
eb96f0429f | ||
|
|
960be3e081 | ||
|
|
2a91c7229a | ||
|
|
6517ed72b4 | ||
|
|
c125d1489c | ||
|
|
be1f1e8781 | ||
|
|
259f499e25 | ||
|
|
fc05c38b9b | ||
|
|
da8d25d80a | ||
|
|
6981599103 | ||
|
|
5557e982bf | ||
|
|
08466218d8 | ||
|
|
1c90c3bbc2 | ||
|
|
aa79247c39 | ||
|
|
04e28f9556 | ||
|
|
d666871d86 | ||
|
|
d81b6ab5dc | ||
|
|
bf27f2d869 | ||
|
|
6e6ef83d60 |
@@ -4,7 +4,7 @@ import inspect
|
||||
import frappe
|
||||
from frappe.utils.user import is_website_user
|
||||
|
||||
__version__ = "15.108.3"
|
||||
__version__ = "15.111.0"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
{
|
||||
"custom_fields": [
|
||||
{
|
||||
"_assign": null,
|
||||
"_comments": null,
|
||||
"_liked_by": null,
|
||||
"_user_tags": null,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"collapsible_depends_on": null,
|
||||
"columns": 0,
|
||||
"creation": "2018-12-28 22:29:21.828090",
|
||||
"default": null,
|
||||
"depends_on": null,
|
||||
"description": null,
|
||||
"docstatus": 0,
|
||||
"dt": "Address",
|
||||
"fetch_from": null,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "tax_category",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"hide_border": 0,
|
||||
"hide_days": 0,
|
||||
"hide_seconds": 0,
|
||||
"idx": 15,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_preview": 0,
|
||||
"in_standard_filter": 0,
|
||||
"insert_after": "fax",
|
||||
"label": "Tax Category",
|
||||
"length": 0,
|
||||
"mandatory_depends_on": null,
|
||||
"modified": "2018-12-28 22:29:21.828090",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Address-tax_category",
|
||||
"no_copy": 0,
|
||||
"options": "Tax Category",
|
||||
"owner": "Administrator",
|
||||
"parent": null,
|
||||
"parentfield": null,
|
||||
"parenttype": null,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": null,
|
||||
"read_only": 0,
|
||||
"read_only_depends_on": null,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0,
|
||||
"width": null
|
||||
},
|
||||
{
|
||||
"_assign": null,
|
||||
"_comments": null,
|
||||
"_liked_by": null,
|
||||
"_user_tags": null,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"collapsible_depends_on": null,
|
||||
"columns": 0,
|
||||
"creation": "2020-10-14 17:41:40.878179",
|
||||
"default": "0",
|
||||
"depends_on": null,
|
||||
"description": null,
|
||||
"docstatus": 0,
|
||||
"dt": "Address",
|
||||
"fetch_from": null,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "is_your_company_address",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"hide_border": 0,
|
||||
"hide_days": 0,
|
||||
"hide_seconds": 0,
|
||||
"idx": 20,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_preview": 0,
|
||||
"in_standard_filter": 0,
|
||||
"insert_after": "linked_with",
|
||||
"label": "Is Your Company Address",
|
||||
"length": 0,
|
||||
"mandatory_depends_on": null,
|
||||
"modified": "2020-10-14 17:41:40.878179",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Address-is_your_company_address",
|
||||
"no_copy": 0,
|
||||
"options": null,
|
||||
"owner": "Administrator",
|
||||
"parent": null,
|
||||
"parentfield": null,
|
||||
"parenttype": null,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": null,
|
||||
"read_only": 0,
|
||||
"read_only_depends_on": null,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0,
|
||||
"width": null
|
||||
}
|
||||
],
|
||||
"custom_perms": [],
|
||||
"doctype": "Address",
|
||||
"property_setters": [],
|
||||
"sync_on_migrate": 1
|
||||
}
|
||||
@@ -517,6 +517,7 @@ def get_account_autoname(account_number, account_name, company):
|
||||
def update_account_number(name, account_name, account_number=None, from_descendant=False):
|
||||
_ensure_idle_system()
|
||||
account = frappe.get_cached_doc("Account", name)
|
||||
account.check_permission("write")
|
||||
if not account:
|
||||
return
|
||||
|
||||
|
||||
@@ -0,0 +1,449 @@
|
||||
{
|
||||
"country_code": "nz",
|
||||
"name": "New Zealand - Chart of Accounts with Account Numbers",
|
||||
"disabled": "No",
|
||||
"tree": {
|
||||
"Application of Funds (Assets)": {
|
||||
"Current Assets": {
|
||||
"Bank Accounts": {
|
||||
"Business Transaction Account": {
|
||||
"account_number": "11011",
|
||||
"account_type": "Bank"
|
||||
},
|
||||
"Business Savings Account": {
|
||||
"account_number": "11012",
|
||||
"account_type": "Bank"
|
||||
},
|
||||
"account_number": "11010",
|
||||
"is_group": 1
|
||||
},
|
||||
"Cash on Hand": {
|
||||
"account_number": "11020",
|
||||
"account_type": "Cash"
|
||||
},
|
||||
"Accounts Receivable": {
|
||||
"Debtors": {
|
||||
"account_number": "11210",
|
||||
"account_type": "Receivable"
|
||||
},
|
||||
"Provision for Doubtful Debts": {
|
||||
"account_number": "11220"
|
||||
},
|
||||
"account_number": "11200",
|
||||
"is_group": 1
|
||||
},
|
||||
"Inventory": {
|
||||
"Stock on Hand": {
|
||||
"account_number": "11311",
|
||||
"account_type": "Stock"
|
||||
},
|
||||
"Work In Progress": {
|
||||
"account_number": "11312",
|
||||
"account_type": "Stock"
|
||||
},
|
||||
"account_number": "11310",
|
||||
"account_type": "Stock",
|
||||
"is_group": 1
|
||||
},
|
||||
"Prepayments": {
|
||||
"Prepayments": {
|
||||
"account_number": "11411"
|
||||
},
|
||||
"Supplier Advances": {
|
||||
"account_number": "11412"
|
||||
},
|
||||
"Deferred Expense": {
|
||||
"account_number": "11413"
|
||||
},
|
||||
"account_number": "11410",
|
||||
"is_group": 1
|
||||
},
|
||||
"GST Receivable": {
|
||||
"account_number": "11510",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"Income Tax Receivable": {
|
||||
"account_number": "11520",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"account_number": "11000",
|
||||
"is_group": 1
|
||||
},
|
||||
"Fixed Assets": {
|
||||
"Plant & Equipment": {
|
||||
"Plant & Equipment": {
|
||||
"account_number": "16011",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Accumulated Depreciation - Plant & Equipment": {
|
||||
"account_number": "16012",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"account_number": "16010",
|
||||
"is_group": 1
|
||||
},
|
||||
"Motor Vehicles": {
|
||||
"Motor Vehicles": {
|
||||
"account_number": "16021",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Accumulated Depreciation - Motor Vehicles": {
|
||||
"account_number": "16022",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"account_number": "16020",
|
||||
"is_group": 1
|
||||
},
|
||||
"Office Equipment": {
|
||||
"Office Equipment": {
|
||||
"account_number": "16031",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Accumulated Depreciation - Office Equipment": {
|
||||
"account_number": "16032",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"account_number": "16030",
|
||||
"is_group": 1
|
||||
},
|
||||
"Buildings": {
|
||||
"Buildings": {
|
||||
"account_number": "16041",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Accumulated Depreciation - Buildings": {
|
||||
"account_number": "16042",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"account_number": "16040",
|
||||
"is_group": 1
|
||||
},
|
||||
"Computer Equipment": {
|
||||
"Computer Equipment": {
|
||||
"account_number": "16051",
|
||||
"account_type": "Fixed Asset"
|
||||
},
|
||||
"Accumulated Depreciation - Computer Equipment": {
|
||||
"account_number": "16052",
|
||||
"account_type": "Accumulated Depreciation"
|
||||
},
|
||||
"account_number": "16050",
|
||||
"is_group": 1
|
||||
},
|
||||
"Capital Work in Progress": {
|
||||
"account_number": "16090",
|
||||
"account_type": "Capital Work in Progress"
|
||||
},
|
||||
"account_number": "16000",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "10000",
|
||||
"root_type": "Asset"
|
||||
},
|
||||
"Source of Funds (Liabilities)": {
|
||||
"Current Liabilities": {
|
||||
"Accounts Payable": {
|
||||
"Creditors": {
|
||||
"account_number": "21010",
|
||||
"account_type": "Payable"
|
||||
},
|
||||
"account_number": "21000",
|
||||
"is_group": 1
|
||||
},
|
||||
"Goods Received Not Invoiced": {
|
||||
"account_number": "21100",
|
||||
"account_type": "Stock Received But Not Billed"
|
||||
},
|
||||
"Asset Received Not Invoiced": {
|
||||
"account_number": "21110",
|
||||
"account_type": "Asset Received But Not Billed"
|
||||
},
|
||||
"Service Received Not Invoiced": {
|
||||
"account_number": "21120",
|
||||
"account_type": "Service Received But Not Billed"
|
||||
},
|
||||
"Accrued Expenses": {
|
||||
"account_number": "21200"
|
||||
},
|
||||
"Wages Payable": {
|
||||
"account_number": "21300"
|
||||
},
|
||||
"PAYE Payable": {
|
||||
"account_number": "22010"
|
||||
},
|
||||
"KiwiSaver Payable": {
|
||||
"account_number": "22020"
|
||||
},
|
||||
"ACC Payable": {
|
||||
"account_number": "22030"
|
||||
},
|
||||
"Credit Cards": {
|
||||
"Business Credit Card": {
|
||||
"account_number": "22110"
|
||||
},
|
||||
"account_number": "22100",
|
||||
"is_group": 1
|
||||
},
|
||||
"Customer Advances": {
|
||||
"account_number": "22200"
|
||||
},
|
||||
"Deferred Revenue": {
|
||||
"account_number": "22210"
|
||||
},
|
||||
"Provisional Account": {
|
||||
"account_number": "22220"
|
||||
},
|
||||
"Tax Liabilities": {
|
||||
"GST Payable": {
|
||||
"account_number": "22310",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"GST Suspense": {
|
||||
"account_number": "22320",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"FBT Payable": {
|
||||
"account_number": "22330",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"Income Tax Payable": {
|
||||
"account_number": "22340",
|
||||
"account_type": "Tax"
|
||||
},
|
||||
"account_number": "22300",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "21500",
|
||||
"is_group": 1
|
||||
},
|
||||
"Non-Current Liabilities": {
|
||||
"Bank Loans": {
|
||||
"Bank Loan": {
|
||||
"account_number": "25011"
|
||||
},
|
||||
"account_number": "25010",
|
||||
"is_group": 1
|
||||
},
|
||||
"Lease Liabilities": {
|
||||
"Lease Liability": {
|
||||
"account_number": "25021"
|
||||
},
|
||||
"account_number": "25020",
|
||||
"is_group": 1
|
||||
},
|
||||
"Shareholder Loans": {
|
||||
"Shareholder Loan": {
|
||||
"account_number": "25031"
|
||||
},
|
||||
"account_number": "25030",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "25000",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "20000",
|
||||
"root_type": "Liability"
|
||||
},
|
||||
"Equity": {
|
||||
"Share Capital": {
|
||||
"account_number": "31010",
|
||||
"account_type": "Equity"
|
||||
},
|
||||
"Drawings": {
|
||||
"account_number": "31020",
|
||||
"account_type": "Equity"
|
||||
},
|
||||
"Current Year Earnings": {
|
||||
"account_number": "35010",
|
||||
"account_type": "Equity"
|
||||
},
|
||||
"Retained Earnings": {
|
||||
"account_number": "35020",
|
||||
"account_type": "Equity"
|
||||
},
|
||||
"account_number": "30000",
|
||||
"root_type": "Equity"
|
||||
},
|
||||
"Income": {
|
||||
"Sales": {
|
||||
"account_number": "41010",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Other Income": {
|
||||
"Interest Income": {
|
||||
"account_number": "47010",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Rounding Gain/Loss": {
|
||||
"account_number": "47020",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"Foreign Exchange Gain": {
|
||||
"account_number": "47030",
|
||||
"account_type": "Income Account"
|
||||
},
|
||||
"account_number": "47000",
|
||||
"is_group": 1
|
||||
},
|
||||
"account_number": "40000",
|
||||
"root_type": "Income"
|
||||
},
|
||||
"Expenses": {
|
||||
"Cost of Goods Sold": {
|
||||
"Purchases": {
|
||||
"account_number": "51010",
|
||||
"account_type": "Cost of Goods Sold"
|
||||
},
|
||||
"Freight Inwards": {
|
||||
"account_number": "51020",
|
||||
"account_type": "Expenses Included In Valuation"
|
||||
},
|
||||
"Duty and Landing Costs": {
|
||||
"account_number": "51030",
|
||||
"account_type": "Expenses Included In Valuation"
|
||||
},
|
||||
"Stock Adjustment": {
|
||||
"account_number": "51040",
|
||||
"account_type": "Stock Adjustment"
|
||||
},
|
||||
"Stock Write Off": {
|
||||
"account_number": "51050",
|
||||
"account_type": "Stock Adjustment"
|
||||
},
|
||||
"account_number": "51000",
|
||||
"account_type": "Cost of Goods Sold",
|
||||
"is_group": 1
|
||||
},
|
||||
"Operating Expenses": {
|
||||
"Wages & Salaries": {
|
||||
"account_number": "61010",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"KiwiSaver Employer Contribution": {
|
||||
"account_number": "61020",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"ACC Levies": {
|
||||
"account_number": "61030",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Rent": {
|
||||
"account_number": "65010",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Power": {
|
||||
"account_number": "65020",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Telephone": {
|
||||
"account_number": "66010",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Insurance": {
|
||||
"account_number": "64010",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Accounting Fees": {
|
||||
"account_number": "64020",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Legal Fees": {
|
||||
"account_number": "64030",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Advertising and Marketing": {
|
||||
"account_number": "65030",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Repairs and Maintenance": {
|
||||
"account_number": "65040",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Freight and Courier": {
|
||||
"account_number": "65050",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Operating Costs": {
|
||||
"account_number": "65060",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"account_number": "60000",
|
||||
"is_group": 1
|
||||
},
|
||||
"Depreciation and Amortisation": {
|
||||
"Depreciation - Plant & Equipment": {
|
||||
"account_number": "62010",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Depreciation - Motor Vehicles": {
|
||||
"account_number": "62020",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Depreciation - Office Equipment": {
|
||||
"account_number": "62030",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"Depreciation - Computer Equipment": {
|
||||
"account_number": "62040",
|
||||
"account_type": "Depreciation"
|
||||
},
|
||||
"account_number": "62000",
|
||||
"is_group": 1
|
||||
},
|
||||
"Finance Costs": {
|
||||
"Bank Charges": {
|
||||
"account_number": "67010",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Interest Expense": {
|
||||
"account_number": "67020",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Rounding Off": {
|
||||
"account_number": "67030",
|
||||
"account_type": "Round Off"
|
||||
},
|
||||
"Payment Discounts": {
|
||||
"account_number": "67040",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"account_number": "67000",
|
||||
"is_group": 1
|
||||
},
|
||||
"Income Tax Expense": {
|
||||
"account_number": "81010",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Foreign Exchange": {
|
||||
"Exchange Gain/Loss": {
|
||||
"account_number": "82010",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Unrealized Exchange Gain/Loss": {
|
||||
"account_number": "82020",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"account_number": "82000",
|
||||
"is_group": 1
|
||||
},
|
||||
"Bad Debts": {
|
||||
"account_number": "83010",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Write Off": {
|
||||
"account_number": "83020",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Gain/Loss on Asset Disposal": {
|
||||
"account_number": "83030",
|
||||
"account_type": "Expense Account"
|
||||
},
|
||||
"Expenses Included In Asset Valuation": {
|
||||
"account_number": "84010",
|
||||
"account_type": "Expenses Included In Asset Valuation"
|
||||
},
|
||||
"account_number": "50000",
|
||||
"root_type": "Expense"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "format:Bank Statement Import on {creation}",
|
||||
"beta": 1,
|
||||
"creation": "2019-08-04 14:16:08.318714",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
@@ -211,10 +210,11 @@
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"links": [],
|
||||
"modified": "2024-06-25 17:32:07.658250",
|
||||
"modified": "2026-05-30 20:51:10.353723",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Statement Import",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -230,7 +230,9 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,14 @@ frappe.provide("erpnext.cheque_print");
|
||||
frappe.ui.form.on("Cheque Print Template", {
|
||||
refresh: function (frm) {
|
||||
if (!frm.doc.__islocal) {
|
||||
frm.add_custom_button(
|
||||
frm.doc.has_print_format ? __("Update Print Format") : __("Create Print Format"),
|
||||
function () {
|
||||
erpnext.cheque_print.view_cheque_print(frm);
|
||||
}
|
||||
).addClass("btn-primary");
|
||||
if (frappe.user.has_role("System Manager")) {
|
||||
frm.add_custom_button(
|
||||
frm.doc.has_print_format ? __("Update Print Format") : __("Create Print Format"),
|
||||
function () {
|
||||
erpnext.cheque_print.view_cheque_print(frm);
|
||||
}
|
||||
).addClass("btn-primary");
|
||||
}
|
||||
|
||||
$(frm.fields_dict.cheque_print_preview.wrapper).empty();
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -48,6 +48,8 @@ class ChequePrintTemplate(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_or_update_cheque_print_format(template_name):
|
||||
frappe.only_for("System Manager")
|
||||
|
||||
if not frappe.db.exists("Print Format", template_name):
|
||||
cheque_print = frappe.new_doc("Print Format")
|
||||
cheque_print.update(
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"actions": [],
|
||||
"allow_events_in_timeline": 1,
|
||||
"autoname": "naming_series:",
|
||||
"beta": 1,
|
||||
"creation": "2019-07-05 16:34:31.013238",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
@@ -400,7 +399,7 @@
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-11-26 13:46:07.760867",
|
||||
"modified": "2026-05-30 20:40:30.851842",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Dunning",
|
||||
@@ -449,9 +448,10 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"title_field": "customer_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"beta": 1,
|
||||
"creation": "2019-12-04 04:59:08.003664",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
@@ -107,7 +106,7 @@
|
||||
"link_fieldname": "dunning_type"
|
||||
}
|
||||
],
|
||||
"modified": "2021-11-13 00:25:35.659283",
|
||||
"modified": "2026-05-30 20:40:09.952533",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Dunning Type",
|
||||
@@ -151,7 +150,9 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -378,15 +378,17 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro
|
||||
|
||||
accounts_add(doc, cdt, cdn) {
|
||||
var row = frappe.get_doc(cdt, cdn);
|
||||
row.exchange_rate = 1;
|
||||
$.each(doc.accounts, function (i, d) {
|
||||
if (d.account && d.party && d.party_type) {
|
||||
row.account = d.account;
|
||||
row.party = d.party;
|
||||
row.party_type = d.party_type;
|
||||
row.exchange_rate = d.exchange_rate;
|
||||
}
|
||||
});
|
||||
if (!row.exchange_rate) row.exchange_rate = 1;
|
||||
if (!row.account) {
|
||||
$.each(doc.accounts, function (i, d) {
|
||||
if (d.account && d.party && d.party_type) {
|
||||
row.account = d.account;
|
||||
row.party = d.party;
|
||||
row.party_type = d.party_type;
|
||||
row.exchange_rate = d.exchange_rate;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// set difference
|
||||
if (doc.difference) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_copy": 1,
|
||||
"beta": 1,
|
||||
"creation": "2017-08-29 02:22:54.947711",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
@@ -64,10 +64,10 @@
|
||||
"options": "Cost Center"
|
||||
},
|
||||
{
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
@@ -82,7 +82,8 @@
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"issingle": 1,
|
||||
"modified": "2022-01-04 15:25:06.053187",
|
||||
"links": [],
|
||||
"modified": "2026-05-30 20:43:36.282738",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Opening Invoice Creation Tool",
|
||||
@@ -99,7 +100,9 @@
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -31,6 +31,7 @@ class OpeningInvoiceCreationTool(Document):
|
||||
create_missing_party: DF.Check
|
||||
invoice_type: DF.Literal["Sales", "Purchase"]
|
||||
invoices: DF.Table[OpeningInvoiceCreationToolItem]
|
||||
project: DF.Link | None
|
||||
# end: auto-generated types
|
||||
|
||||
def onload(self):
|
||||
|
||||
@@ -822,11 +822,14 @@ frappe.ui.form.on("Payment Entry", {
|
||||
frm.set_value("base_paid_amount", flt(frm.doc.paid_amount) * flt(frm.doc.source_exchange_rate));
|
||||
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
|
||||
if (!frm.doc.received_amount) {
|
||||
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("received_amount", frm.doc.paid_amount);
|
||||
} else if (company_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
|
||||
if (company_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("received_amount", frm.doc.base_paid_amount);
|
||||
frm.set_value("base_received_amount", frm.doc.base_paid_amount);
|
||||
} else if (frm.doc.target_exchange_rate) {
|
||||
frm.set_value(
|
||||
"received_amount",
|
||||
flt(frm.doc.base_paid_amount) / flt(frm.doc.target_exchange_rate)
|
||||
);
|
||||
}
|
||||
}
|
||||
frm.trigger("reset_received_amount");
|
||||
@@ -843,15 +846,14 @@ frappe.ui.form.on("Payment Entry", {
|
||||
);
|
||||
|
||||
if (!frm.doc.paid_amount) {
|
||||
if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency) {
|
||||
frm.set_value("paid_amount", frm.doc.received_amount);
|
||||
if (frm.doc.target_exchange_rate) {
|
||||
frm.set_value("source_exchange_rate", frm.doc.target_exchange_rate);
|
||||
}
|
||||
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
|
||||
} else if (company_currency == frm.doc.paid_from_account_currency) {
|
||||
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
|
||||
if (company_currency == frm.doc.paid_from_account_currency) {
|
||||
frm.set_value("paid_amount", frm.doc.base_received_amount);
|
||||
frm.set_value("base_paid_amount", frm.doc.base_received_amount);
|
||||
} else if (frm.doc.source_exchange_rate) {
|
||||
frm.set_value(
|
||||
"paid_amount",
|
||||
flt(frm.doc.base_received_amount) / flt(frm.doc.source_exchange_rate)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1771,6 +1773,35 @@ frappe.ui.form.on("Payment Entry", {
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
before_cancel: function (frm) {
|
||||
return new Promise((resolve, reject) => {
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_linked_bank_transactions",
|
||||
args: { payment_entry: frm.doc.name },
|
||||
callback: function (r) {
|
||||
const linked = r.message || [];
|
||||
if (!linked.length) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const bt_links = linked
|
||||
.map((name) => frappe.utils.get_form_link("Bank Transaction", name, true))
|
||||
.join(", ");
|
||||
frappe.confirm(
|
||||
__(
|
||||
"This Payment Entry is reconciled with {0}. Cancelling will automatically unreconcile it. Do you want to proceed?",
|
||||
[bt_links]
|
||||
),
|
||||
() => resolve(),
|
||||
() => reject(),
|
||||
__("Yes"),
|
||||
__("No")
|
||||
);
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Payment Entry Reference", {
|
||||
|
||||
@@ -2279,6 +2279,9 @@ def get_outstanding_reference_documents(args, validate=False):
|
||||
if args.get("party_type") == "Member":
|
||||
return
|
||||
|
||||
if args.get("party_type") and args.get("party"):
|
||||
frappe.has_permission(args["party_type"], "read", args["party"], throw=True)
|
||||
|
||||
if not args.get("get_outstanding_invoices") and not args.get("get_orders_to_be_billed"):
|
||||
args["get_outstanding_invoices"] = True
|
||||
|
||||
@@ -2788,7 +2791,8 @@ def get_reference_details(
|
||||
):
|
||||
total_amount = outstanding_amount = exchange_rate = account = None
|
||||
|
||||
ref_doc = frappe.get_doc(reference_doctype, reference_name)
|
||||
frappe.has_permission(reference_doctype, "read", reference_name, throw=True)
|
||||
ref_doc = frappe.get_lazy_doc(reference_doctype, reference_name)
|
||||
company_currency = ref_doc.get("company_currency") or erpnext.get_company_currency(ref_doc.company)
|
||||
|
||||
# Only applies for Reverse Payment Entries
|
||||
@@ -3599,3 +3603,16 @@ def make_payment_order(source_name, target_doc=None):
|
||||
@erpnext.allow_regional
|
||||
def add_regional_gl_entries(gl_entries, doc):
|
||||
return
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_linked_bank_transactions(payment_entry: str) -> list:
|
||||
frappe.has_permission("Payment Entry", ptype="read", doc=payment_entry, throw=True)
|
||||
return frappe.get_all(
|
||||
"Bank Transaction Payments",
|
||||
filters={
|
||||
"payment_document": "Payment Entry",
|
||||
"payment_entry": payment_entry,
|
||||
},
|
||||
pluck="parent",
|
||||
)
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.query_builder.functions import Count, Sum
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import nowdate
|
||||
from frappe.utils import add_days, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
@@ -94,6 +95,7 @@ class TestPaymentLedgerEntry(FrappeTestCase):
|
||||
posting_date = nowdate()
|
||||
|
||||
sinv = create_sales_invoice(
|
||||
posting_date=posting_date,
|
||||
qty=qty,
|
||||
rate=rate,
|
||||
company=self.company,
|
||||
@@ -535,3 +537,82 @@ class TestPaymentLedgerEntry(FrappeTestCase):
|
||||
# with references removed, deletion should be possible
|
||||
so.delete()
|
||||
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, so.doctype, so.name)
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{"enable_immutable_ledger": 1},
|
||||
)
|
||||
def test_reverse_entries_on_cancel_for_immutable_ledger(self):
|
||||
invoice_posting_date = add_days(nowdate(), -5)
|
||||
gle = qb.DocType("GL Entry")
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
|
||||
si = self.create_sales_invoice(qty=1, rate=100, posting_date=invoice_posting_date)
|
||||
|
||||
gles_before = (
|
||||
qb.from_(gle)
|
||||
.select(
|
||||
Count(gle.name),
|
||||
)
|
||||
.where((gle.voucher_type == si.doctype) & (gle.voucher_no == si.name) & (gle.is_cancelled == 0))
|
||||
.run()[0][0]
|
||||
)
|
||||
ples_before = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
Count(ple.name),
|
||||
)
|
||||
.where((ple.voucher_type == si.doctype) & (ple.voucher_no == si.name) & (ple.delinked.eq(0)))
|
||||
.run()[0][0]
|
||||
)
|
||||
|
||||
si.cancel()
|
||||
|
||||
gles_after = (
|
||||
qb.from_(gle)
|
||||
.select(Count(gle.account))
|
||||
.where((gle.voucher_type == si.doctype) & (gle.voucher_no == si.name) & (gle.is_cancelled == 0))
|
||||
.run()[0][0]
|
||||
)
|
||||
self.assertEqual(gles_after, gles_before * 2)
|
||||
|
||||
ples_after = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
Count(ple.name),
|
||||
)
|
||||
.where((ple.voucher_type == si.doctype) & (ple.voucher_no == si.name) & (ple.delinked.eq(0)))
|
||||
.run()[0][0]
|
||||
)
|
||||
self.assertEqual(ples_after, ples_before * 2)
|
||||
|
||||
# assert debit/credit are reversed
|
||||
gl_entries = (
|
||||
qb.from_(gle)
|
||||
.select(gle.account, Sum(gle.debit).as_("total_debit"), Sum(gle.credit).as_("total_credit"))
|
||||
.where((gle.voucher_type == si.doctype) & (gle.voucher_no == si.name) & (gle.is_cancelled == 0))
|
||||
.groupby(gle.account)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
for gl in gl_entries:
|
||||
with self.subTest(gl=gl):
|
||||
self.assertEqual(gl.total_debit, gl.total_credit)
|
||||
|
||||
# assert amounts are reversed
|
||||
pl_entries = (
|
||||
qb.from_(ple)
|
||||
.select(ple.account, Sum(ple.amount).as_("total_amount"))
|
||||
.where((ple.voucher_type == si.doctype) & (ple.voucher_no == si.name) & (ple.delinked == 0))
|
||||
.groupby(ple.account)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
for pl in pl_entries:
|
||||
with self.subTest(pl=pl):
|
||||
self.assertEqual(pl.total_amount, 0)
|
||||
|
||||
self.assertFalse(
|
||||
frappe.db.exists(
|
||||
"Payment Ledger Entry",
|
||||
{"voucher_type": si.doctype, "voucher_no": si.name, "delinked": 1},
|
||||
)
|
||||
)
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import change_settings
|
||||
from frappe.utils import today
|
||||
|
||||
from erpnext.accounts.doctype.finance_book.test_finance_book import create_finance_book
|
||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.general_ledger import make_reverse_gl_entries
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
|
||||
|
||||
@@ -351,6 +353,51 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
||||
|
||||
return pcv
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{"enable_immutable_ledger": 1},
|
||||
)
|
||||
def test_immutable_ledger_reverse_entry_uses_passed_posting_date_after_pcv(self):
|
||||
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
|
||||
frappe.db.sql("delete from `tabPeriod Closing Voucher` where company='Test PCV Company'")
|
||||
|
||||
company = create_company()
|
||||
cost_center = create_cost_center("Test Cost Center 1")
|
||||
|
||||
jv = make_journal_entry(
|
||||
posting_date="2021-03-15",
|
||||
amount=400,
|
||||
account1="Cash - TPC",
|
||||
account2="Sales - TPC",
|
||||
cost_center=cost_center,
|
||||
company=company,
|
||||
save=False,
|
||||
)
|
||||
jv.company = company
|
||||
jv.save()
|
||||
jv.submit()
|
||||
|
||||
self.make_period_closing_voucher(posting_date="2021-03-31")
|
||||
|
||||
# Passed posting_date is after PCV end date, so cancellation should not fail.
|
||||
make_reverse_gl_entries(
|
||||
voucher_type="Journal Entry",
|
||||
voucher_no=jv.name,
|
||||
posting_date="2022-01-01",
|
||||
)
|
||||
|
||||
totals_after_cancel = frappe.db.sql(
|
||||
"""
|
||||
select sum(debit) as total_debit, sum(credit) as total_credit
|
||||
from `tabGL Entry`
|
||||
where voucher_type=%s and voucher_no=%s and is_cancelled=0
|
||||
""",
|
||||
("Journal Entry", jv.name),
|
||||
as_dict=True,
|
||||
)[0]
|
||||
|
||||
self.assertEqual(totals_after_cancel.total_debit, totals_after_cancel.total_credit)
|
||||
|
||||
|
||||
def create_company():
|
||||
company = frappe.get_doc(
|
||||
|
||||
@@ -202,15 +202,14 @@ class POSProfile(Document):
|
||||
def set_defaults(self, include_current_pos=True):
|
||||
frappe.defaults.clear_default("is_pos")
|
||||
|
||||
if not include_current_pos:
|
||||
condition = " where pfu.name != '%s' and pfu.default = 1 " % self.name.replace("'", "'")
|
||||
else:
|
||||
condition = " where pfu.default = 1 "
|
||||
pfu = frappe.qb.DocType("POS Profile User")
|
||||
|
||||
pos_view_users = frappe.db.sql_list(
|
||||
f"""select pfu.user
|
||||
from `tabPOS Profile User` as pfu {condition}"""
|
||||
)
|
||||
query = frappe.qb.from_(pfu).select(pfu.user).where(pfu.default == 1)
|
||||
|
||||
if not include_current_pos:
|
||||
query = query.where(pfu.name != self.name)
|
||||
|
||||
pos_view_users = query.run(as_list=1, pluck=True)
|
||||
|
||||
for user in pos_view_users:
|
||||
if user:
|
||||
|
||||
@@ -151,13 +151,13 @@
|
||||
"label": "Default Advance Account",
|
||||
"mandatory_depends_on": "doc.party_type",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
"reqd": 0
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-01-08 08:22:14.798085",
|
||||
"modified": "2026-05-16 11:43:12.758685",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Payment Reconciliation",
|
||||
|
||||
@@ -23,7 +23,7 @@ class ProcessPaymentReconciliation(Document):
|
||||
bank_cash_account: DF.Link | None
|
||||
company: DF.Link
|
||||
cost_center: DF.Link | None
|
||||
default_advance_account: DF.Link
|
||||
default_advance_account: DF.Link | None
|
||||
error_log: DF.LongText | None
|
||||
from_invoice_date: DF.Date | None
|
||||
from_payment_date: DF.Date | None
|
||||
@@ -215,10 +215,7 @@ def trigger_reconciliation_for_queued_docs():
|
||||
fields = ["company", "party_type", "party", "receivable_payable_account", "default_advance_account"]
|
||||
|
||||
def get_filters_as_tuple(fields, doc):
|
||||
filters = ()
|
||||
for x in fields:
|
||||
filters += tuple(doc.get(x))
|
||||
return filters
|
||||
return tuple(doc.get(x) or "" for x in fields)
|
||||
|
||||
for x in all_queued:
|
||||
doc = frappe.get_doc("Process Payment Reconciliation", x)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_bulk_edit": 1,
|
||||
"autoname": "format:Process-PCV-{###}",
|
||||
"creation": "2025-09-25 15:44:03.534699",
|
||||
"doctype": "DocType",
|
||||
@@ -7,11 +8,13 @@
|
||||
"field_order": [
|
||||
"parent_pcv",
|
||||
"status",
|
||||
"amended_from",
|
||||
"section_normal_balances",
|
||||
"p_l_closing_balance",
|
||||
"normal_balances",
|
||||
"bs_closing_balance",
|
||||
"z_opening_balances",
|
||||
"amended_from"
|
||||
"normal_balances",
|
||||
"section_opening_balances",
|
||||
"z_opening_balances"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -64,17 +67,27 @@
|
||||
"fieldname": "bs_closing_balance",
|
||||
"fieldtype": "JSON",
|
||||
"label": "Balance Sheet Closing Balance"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_normal_balances",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Normal Balances"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_opening_balances",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Opening Balances"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-11-05 11:40:24.996403",
|
||||
"modified": "2026-06-01 12:16:37.374412",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Period Closing Voucher",
|
||||
"naming_rule": "Expression",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
||||
@@ -134,9 +134,10 @@ def pause_pcv_processing(docname: str):
|
||||
ppcv = qb.DocType("Process Period Closing Voucher")
|
||||
qb.update(ppcv).set(ppcv.status, "Paused").where(ppcv.name.eq(docname)).run()
|
||||
|
||||
# If a date is stuck in 'Running' state, this will allow it to procced.
|
||||
if queued_dates := frappe.db.get_all(
|
||||
"Process Period Closing Voucher Detail",
|
||||
filters={"parent": docname, "status": "Queued"},
|
||||
filters={"parent": docname, "status": ["in", ["Queued", "Running"]]},
|
||||
pluck="name",
|
||||
):
|
||||
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
|
||||
@@ -170,6 +171,9 @@ def resume_pcv_processing(docname: str):
|
||||
ppcvd = qb.DocType("Process Period Closing Voucher Detail")
|
||||
qb.update(ppcvd).set(ppcvd.status, "Queued").where(ppcvd.name.isin(paused_dates)).run()
|
||||
start_pcv_processing(docname)
|
||||
else:
|
||||
# If a parent doc is stuck in 'Running' state, will allow it to proceed.
|
||||
schedule_next_date(docname)
|
||||
|
||||
|
||||
def update_default_dimensions(dimension_fields, gl_entry, dimension_values):
|
||||
@@ -285,7 +289,21 @@ def schedule_next_date(docname: str):
|
||||
)
|
||||
# Ensure both normal and opening balances are processed for all dates
|
||||
if total_no_of_dates == completed:
|
||||
summarize_and_post_ledger_entries(docname)
|
||||
from erpnext.accounts.doctype.process_payment_reconciliation.process_payment_reconciliation import (
|
||||
is_job_running,
|
||||
)
|
||||
|
||||
job_name = f"summarize_{docname}"
|
||||
if not is_job_running(job_name):
|
||||
frappe.enqueue(
|
||||
method="erpnext.accounts.doctype.process_period_closing_voucher.process_period_closing_voucher.summarize_and_post_ledger_entries",
|
||||
queue="long",
|
||||
timeout="3600",
|
||||
is_async=True,
|
||||
job_name=job_name,
|
||||
enqueue_after_commit=True,
|
||||
docname=docname,
|
||||
)
|
||||
|
||||
|
||||
def make_dict_json_compliant(dimension_wise_balance) -> dict:
|
||||
@@ -541,6 +559,9 @@ def process_individual_date(docname: str, date, report_type, parentfield):
|
||||
|
||||
if parentfield == "z_opening_balances":
|
||||
query = query.where(gle.is_opening.eq("Yes"))
|
||||
else:
|
||||
# Keep balances aligned with legacy PCV logic (non-opening transactions only)
|
||||
query = query.where(gle.is_opening.eq("No"))
|
||||
|
||||
query = query.groupby(gle.account)
|
||||
for dim in dimensions:
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
// render
|
||||
frappe.listview_settings["Process Period Closing Voucher"] = {
|
||||
add_fields: ["status"],
|
||||
get_indicator: function (doc) {
|
||||
const status_colors = {
|
||||
Queued: "blue",
|
||||
Running: "orange",
|
||||
Paused: "gray",
|
||||
Completed: "green",
|
||||
Cancelled: "red",
|
||||
};
|
||||
return [__(doc.status), status_colors[doc.status], "status,=," + doc.status];
|
||||
},
|
||||
};
|
||||
@@ -99,6 +99,7 @@ class ProcessStatementOfAccounts(Document):
|
||||
|
||||
validate_template(self.subject)
|
||||
validate_template(self.body)
|
||||
validate_template(self.pdf_name)
|
||||
|
||||
if not self.customers:
|
||||
frappe.throw(_("Customers not selected."))
|
||||
@@ -548,6 +549,7 @@ def send_emails(document_name, from_scheduler=False, posting_date=None):
|
||||
|
||||
@frappe.whitelist()
|
||||
def send_auto_email():
|
||||
frappe.has_permission("Process Statement Of Accounts", throw=True)
|
||||
selected = frappe.get_list(
|
||||
"Process Statement Of Accounts",
|
||||
filters={"enable_auto_email": 1},
|
||||
|
||||
@@ -608,6 +608,25 @@ frappe.ui.form.on("Purchase Invoice", {
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("write_off_account", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
report_type: "Profit and Loss",
|
||||
is_group: 0,
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("write_off_cost_center", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
is_group: 0,
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.fields_dict["items"].grid.get_field("deferred_expense_account").get_query = function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
|
||||
@@ -287,6 +287,7 @@ class PurchaseInvoice(BuyingController):
|
||||
self.validate_expense_account()
|
||||
self.set_against_expense_account()
|
||||
self.validate_write_off_account()
|
||||
self.validate_write_off_cost_center()
|
||||
self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount")
|
||||
self.set_status()
|
||||
self.validate_purchase_receipt_if_update_stock()
|
||||
@@ -633,15 +634,16 @@ class PurchaseInvoice(BuyingController):
|
||||
throw(msg, title=_("Mandatory Purchase Order"))
|
||||
|
||||
def pr_required(self):
|
||||
stock_items = self.get_stock_items()
|
||||
if frappe.db.get_value("Buying Settings", None, "pr_required") == "Yes":
|
||||
if frappe.db.get_single_value("Buying Settings", "pr_required") == "Yes":
|
||||
stock_and_asset_items = self.get_stock_items()
|
||||
stock_and_asset_items.extend(self.get_asset_items())
|
||||
if frappe.get_value(
|
||||
"Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_receipt"
|
||||
):
|
||||
return
|
||||
|
||||
for d in self.get("items"):
|
||||
if not d.purchase_receipt and d.item_code in stock_items:
|
||||
if not d.purchase_receipt and d.item_code in stock_and_asset_items:
|
||||
msg = _("Purchase Receipt Required for item {}").format(frappe.bold(d.item_code))
|
||||
msg += "<br><br>"
|
||||
msg += _(
|
||||
@@ -657,6 +659,27 @@ class PurchaseInvoice(BuyingController):
|
||||
if self.write_off_amount and not self.write_off_account:
|
||||
throw(_("Please enter Write Off Account"))
|
||||
|
||||
if not self.write_off_account:
|
||||
return
|
||||
|
||||
doc = frappe.db.get_value(
|
||||
"Account", self.write_off_account, ["report_type", "is_group", "company"], as_dict=True
|
||||
)
|
||||
|
||||
if not doc or doc.report_type != "Profit and Loss" or doc.is_group or doc.company != self.company:
|
||||
throw(_("Please enter a valid Write Off Account"))
|
||||
|
||||
def validate_write_off_cost_center(self):
|
||||
if not self.write_off_cost_center:
|
||||
return
|
||||
|
||||
doc = frappe.db.get_value(
|
||||
"Cost Center", self.write_off_cost_center, ["is_group", "company"], as_dict=True
|
||||
)
|
||||
|
||||
if not doc or doc.is_group or doc.company != self.company:
|
||||
throw(_("Please enter a valid Write Off Cost Center"))
|
||||
|
||||
def check_prev_docstatus(self):
|
||||
for d in self.get("items"):
|
||||
if d.purchase_order:
|
||||
@@ -737,6 +760,7 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
def validate_for_repost(self):
|
||||
self.validate_write_off_account()
|
||||
self.validate_write_off_cost_center()
|
||||
self.validate_expense_account()
|
||||
validate_docs_for_voucher_types(["Purchase Invoice"])
|
||||
validate_docs_for_deferred_accounting([], [self.name])
|
||||
@@ -850,7 +874,9 @@ class PurchaseInvoice(BuyingController):
|
||||
if update_outstanding == "No":
|
||||
update_voucher_outstanding(
|
||||
voucher_type=self.doctype,
|
||||
voucher_no=self.return_against if cint(self.is_return) and self.return_against else self.name,
|
||||
voucher_no=self.return_against
|
||||
if (cint(self.is_return) and self.return_against)
|
||||
else self.name,
|
||||
account=self.credit_to,
|
||||
party_type="Supplier",
|
||||
party=self.supplier,
|
||||
@@ -1533,6 +1559,9 @@ class PurchaseInvoice(BuyingController):
|
||||
def make_payment_gl_entries(self, gl_entries):
|
||||
# Make Cash GL Entries
|
||||
if cint(self.is_paid) and self.cash_bank_account and self.paid_amount:
|
||||
against_voucher = self.name
|
||||
if self.is_return and self.return_against and not self.update_outstanding_for_self:
|
||||
against_voucher = self.return_against
|
||||
bank_account_currency = get_account_currency(self.cash_bank_account)
|
||||
# CASH, make payment entries
|
||||
gl_entries.append(
|
||||
@@ -1547,9 +1576,7 @@ class PurchaseInvoice(BuyingController):
|
||||
if self.party_account_currency == self.company_currency
|
||||
else self.paid_amount,
|
||||
"debit_in_transaction_currency": self.paid_amount,
|
||||
"against_voucher": self.return_against
|
||||
if cint(self.is_return) and self.return_against
|
||||
else self.name,
|
||||
"against_voucher": against_voucher,
|
||||
"against_voucher_type": self.doctype,
|
||||
"cost_center": self.cost_center,
|
||||
"project": self.project,
|
||||
|
||||
@@ -180,12 +180,31 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends (
|
||||
: "Inter Company Purchase Invoice";
|
||||
|
||||
me.frm.add_custom_button(
|
||||
button_label,
|
||||
__(button_label),
|
||||
function () {
|
||||
me.make_inter_company_invoice();
|
||||
},
|
||||
__("Create")
|
||||
);
|
||||
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.get_received_items",
|
||||
args: {
|
||||
reference_name: me.frm.doc.name,
|
||||
doctype: "Purchase Invoice",
|
||||
reference_fieldname: "sales_invoice_item",
|
||||
},
|
||||
callback: function (r) {
|
||||
if (r.exc) return;
|
||||
const received_items = r.message || {};
|
||||
const has_pending_qty = me.frm.doc.items.some(
|
||||
(item) => flt(item.qty) - flt(received_items[item.name] || 0) > 0
|
||||
);
|
||||
if (!has_pending_qty) {
|
||||
me.frm.remove_custom_button(__(button_label), __("Create"));
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,12 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category
|
||||
get_party_tax_withholding_details,
|
||||
)
|
||||
from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center
|
||||
from erpnext.accounts.party import get_due_date, get_party_account, get_party_details
|
||||
from erpnext.accounts.party import (
|
||||
CROSS_PARTY_FIELD_NO_MAP,
|
||||
get_due_date,
|
||||
get_party_account,
|
||||
get_party_details,
|
||||
)
|
||||
from erpnext.accounts.utils import (
|
||||
cancel_exchange_gain_loss_journal,
|
||||
get_account_currency,
|
||||
@@ -2526,7 +2531,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
"rate": "rate",
|
||||
},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: doc.qty > 0,
|
||||
"condition": lambda doc: doc.qty - received_items.get(doc.name, 0.0) > 0,
|
||||
}
|
||||
|
||||
if doctype in ["Sales Invoice", "Sales Order"]:
|
||||
@@ -2559,18 +2564,25 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
"doctype": target_doctype,
|
||||
"postprocess": update_details,
|
||||
"set_target_warehouse": "set_from_warehouse",
|
||||
"field_no_map": ["taxes_and_charges", "set_warehouse", "shipping_address", "cost_center"],
|
||||
"field_no_map": [*CROSS_PARTY_FIELD_NO_MAP, "set_warehouse", "cost_center"],
|
||||
},
|
||||
doctype + " Item": item_field_map,
|
||||
},
|
||||
target_doc,
|
||||
set_missing_values,
|
||||
)
|
||||
|
||||
if not doclist.get("items"):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Cannot create Intercompany {0}. All items in the source {1} have already been fully invoiced. "
|
||||
"Please check the existing linked {2}s."
|
||||
).format(target_doctype, doctype, target_doctype)
|
||||
)
|
||||
return doclist
|
||||
|
||||
|
||||
def get_received_items(reference_name, doctype, reference_fieldname):
|
||||
@frappe.whitelist()
|
||||
def get_received_items(reference_name: str, doctype: str, reference_fieldname: str):
|
||||
reference_field = "inter_company_invoice_reference"
|
||||
if doctype == "Purchase Order":
|
||||
reference_field = "inter_company_order_reference"
|
||||
@@ -2583,20 +2595,19 @@ def get_received_items(reference_name, doctype, reference_fieldname):
|
||||
target_doctypes = frappe.get_all(
|
||||
doctype,
|
||||
filters=filters,
|
||||
as_list=True,
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
received_items_map = {}
|
||||
if target_doctypes:
|
||||
target_doctypes = list(target_doctypes[0])
|
||||
|
||||
received_items_map = frappe._dict(
|
||||
frappe.get_all(
|
||||
received_items_data = frappe.get_all(
|
||||
doctype + " Item",
|
||||
filters={"parent": ("in", target_doctypes)},
|
||||
fields=[reference_fieldname, "qty"],
|
||||
as_list=1,
|
||||
)
|
||||
)
|
||||
for item in received_items_data:
|
||||
key = item.get(reference_fieldname)
|
||||
if key:
|
||||
received_items_map[key] = received_items_map.get(key, 0.0) + flt(item.qty)
|
||||
|
||||
return received_items_map
|
||||
|
||||
|
||||
@@ -2690,6 +2690,95 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
self.assertEqual(target_doc.company, "_Test Company 1")
|
||||
self.assertEqual(target_doc.supplier, "_Test Internal Supplier")
|
||||
|
||||
def test_restrict_inter_company_pi_when_sales_invoice_qty_fully_consumed(self):
|
||||
item_code_1 = "_Test IC Item 1"
|
||||
item_code_2 = "_Test IC Item 2"
|
||||
|
||||
create_item(item_code_1, is_stock_item=1)
|
||||
create_item(item_code_2, is_stock_item=1)
|
||||
|
||||
si = create_sales_invoice(
|
||||
company="Wind Power LLC",
|
||||
customer="_Test Internal Customer",
|
||||
item_code=item_code_1,
|
||||
debit_to="Debtors - WP",
|
||||
warehouse="Stores - WP",
|
||||
income_account="Sales - WP",
|
||||
expense_account="Cost of Goods Sold - WP",
|
||||
cost_center="Main - WP",
|
||||
currency="USD",
|
||||
qty=3,
|
||||
do_not_save=1,
|
||||
)
|
||||
si.selling_price_list = "_Test Price List Rest of the World"
|
||||
si.append(
|
||||
"items",
|
||||
{
|
||||
"item_code": item_code_2,
|
||||
"item_name": item_code_2,
|
||||
"description": item_code_2,
|
||||
"warehouse": "Stores - WP",
|
||||
"qty": 2,
|
||||
"uom": "Nos",
|
||||
"stock_uom": "Nos",
|
||||
"rate": 100,
|
||||
"price_list_rate": 100,
|
||||
"income_account": "Sales - WP",
|
||||
"expense_account": "Cost of Goods Sold - WP",
|
||||
"cost_center": "Main - WP",
|
||||
"conversion_factor": 1,
|
||||
},
|
||||
)
|
||||
|
||||
si.submit()
|
||||
|
||||
target_doc = make_inter_company_transaction("Sales Invoice", si.name)
|
||||
|
||||
for item in target_doc.items:
|
||||
item.update(
|
||||
{
|
||||
"expense_account": "Cost of Goods Sold - _TC1",
|
||||
"cost_center": "Main - _TC1",
|
||||
}
|
||||
)
|
||||
|
||||
target_doc.submit()
|
||||
self.assertEqual(len(target_doc.items), 2)
|
||||
self.assertEqual([item.qty for item in target_doc.items], [3, 2])
|
||||
with self.assertRaisesRegex(
|
||||
frappe.ValidationError,
|
||||
"already been fully invoiced",
|
||||
):
|
||||
make_inter_company_transaction("Sales Invoice", si.name)
|
||||
|
||||
def test_inter_company_transaction_does_not_inherit_party_fields(self):
|
||||
"""
|
||||
Party-derived fields on SI (from Customer) must not leak into the mapped PI.
|
||||
"""
|
||||
si = create_sales_invoice(
|
||||
company="Wind Power LLC",
|
||||
customer="_Test Internal Customer",
|
||||
debit_to="Debtors - WP",
|
||||
warehouse="Stores - WP",
|
||||
income_account="Sales - WP",
|
||||
expense_account="Cost of Goods Sold - WP",
|
||||
cost_center="Main - WP",
|
||||
currency="USD",
|
||||
do_not_save=1,
|
||||
)
|
||||
si.selling_price_list = "_Test Price List Rest of the World"
|
||||
si.tax_category = "_Test Tax Category 1"
|
||||
si.language = "ar"
|
||||
si.payment_terms_template = "_Test Payment Term Template"
|
||||
si.submit()
|
||||
|
||||
pi = make_inter_company_transaction("Sales Invoice", si.name)
|
||||
|
||||
supplier = frappe.get_doc("Supplier", "_Test Internal Supplier")
|
||||
self.assertEqual(pi.tax_category or None, supplier.tax_category or None)
|
||||
self.assertEqual(pi.language or None, supplier.language or None)
|
||||
self.assertEqual(pi.payment_terms_template or None, supplier.payment_terms or None)
|
||||
|
||||
def test_inter_company_transaction_without_default_warehouse(self):
|
||||
"Check mapping (expense account) of inter company SI to PI in absence of default warehouse."
|
||||
# setup
|
||||
|
||||
@@ -8,10 +8,13 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.contacts.doctype.address.address import get_default_address
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder import DocType
|
||||
from frappe.query_builder.functions import IfNull
|
||||
from frappe.utils import cstr
|
||||
from frappe.utils.nestedset import get_root_of
|
||||
|
||||
from erpnext.setup.doctype.customer_group.customer_group import get_parent_customer_groups
|
||||
from erpnext.setup.doctype.supplier_group.supplier_group import get_parent_supplier_groups
|
||||
|
||||
|
||||
class IncorrectCustomerGroup(frappe.ValidationError):
|
||||
@@ -174,38 +177,44 @@ def get_party_details(party, party_type, args=None):
|
||||
def get_tax_template(posting_date, args):
|
||||
"""Get matching tax rule"""
|
||||
args = frappe._dict(args)
|
||||
conditions = []
|
||||
|
||||
TaxRule = DocType("Tax Rule")
|
||||
query = frappe.qb.from_(TaxRule).select("*")
|
||||
|
||||
if posting_date:
|
||||
conditions.append(
|
||||
f"""(from_date is null or from_date <= '{posting_date}')
|
||||
and (to_date is null or to_date >= '{posting_date}')"""
|
||||
query = query.where(
|
||||
(TaxRule.from_date.isnull() | (TaxRule.from_date <= posting_date))
|
||||
& (TaxRule.to_date.isnull() | (TaxRule.to_date >= posting_date))
|
||||
)
|
||||
else:
|
||||
conditions.append("(from_date is null) and (to_date is null)")
|
||||
query = query.where(TaxRule.from_date.isnull() & TaxRule.to_date.isnull())
|
||||
|
||||
conditions.append(
|
||||
"ifnull(tax_category, '') = {}".format(frappe.db.escape(cstr(args.get("tax_category")), False))
|
||||
)
|
||||
if "tax_category" in args.keys():
|
||||
del args["tax_category"]
|
||||
def get_group_ancestors(doctype, get_parents, value):
|
||||
if not value:
|
||||
value = get_root_of(doctype)
|
||||
return [""] + [d.name for d in get_parents(value)]
|
||||
|
||||
group_fields = {
|
||||
"customer_group": ("Customer Group", get_parent_customer_groups),
|
||||
"supplier_group": ("Supplier Group", get_parent_supplier_groups),
|
||||
}
|
||||
|
||||
args.setdefault("tax_category", "")
|
||||
|
||||
for key, value in args.items():
|
||||
if key == "use_for_shopping_cart":
|
||||
conditions.append(f"use_for_shopping_cart = {1 if value else 0}")
|
||||
elif key == "customer_group":
|
||||
if not value:
|
||||
value = get_root_of("Customer Group")
|
||||
customer_group_condition = get_customer_group_condition(value)
|
||||
conditions.append(f"ifnull({key}, '') in ('', {customer_group_condition})")
|
||||
query = query.where(TaxRule.use_for_shopping_cart == value)
|
||||
elif key == "tax_category":
|
||||
query = query.where(IfNull(TaxRule.tax_category, "") == (value or ""))
|
||||
elif key in group_fields:
|
||||
doctype, get_parents = group_fields[key]
|
||||
query = query.where(
|
||||
IfNull(TaxRule[key], "").isin(get_group_ancestors(doctype, get_parents, value))
|
||||
)
|
||||
else:
|
||||
conditions.append(f"ifnull({key}, '') in ('', {frappe.db.escape(cstr(value))})")
|
||||
query = query.where(IfNull(TaxRule[key], "").isin(["", value or ""]))
|
||||
|
||||
tax_rule = frappe.db.sql(
|
||||
"""select * from `tabTax Rule`
|
||||
where {}""".format(" and ".join(conditions)),
|
||||
as_dict=True,
|
||||
)
|
||||
tax_rule = query.run(as_dict=True)
|
||||
|
||||
if not tax_rule:
|
||||
return None
|
||||
@@ -234,11 +243,3 @@ def get_tax_template(posting_date, args):
|
||||
return None
|
||||
|
||||
return tax_template
|
||||
|
||||
|
||||
def get_customer_group_condition(customer_group):
|
||||
condition = ""
|
||||
customer_groups = ["%s" % (frappe.db.escape(d.name)) for d in get_parent_customer_groups(customer_group)]
|
||||
if customer_groups:
|
||||
condition = ",".join(["%s"] * len(customer_groups)) % (tuple(customer_groups))
|
||||
return condition
|
||||
|
||||
@@ -72,6 +72,117 @@ class TestTaxRule(unittest.TestCase):
|
||||
"_Test Sales Taxes and Charges Template - _TC",
|
||||
)
|
||||
|
||||
def test_for_parent_supplier_group(self):
|
||||
purchase_template = "_Test Purchase Taxes and Charges Template - _TC"
|
||||
if not frappe.db.exists("Purchase Taxes and Charges Template", purchase_template):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Purchase Taxes and Charges Template",
|
||||
"title": "_Test Purchase Taxes and Charges Template",
|
||||
"company": "_Test Company",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": "_Test Account VAT - _TC",
|
||||
"charge_type": "On Net Total",
|
||||
"description": "VAT",
|
||||
"doctype": "Purchase Taxes and Charges",
|
||||
"cost_center": "Main - _TC",
|
||||
"rate": 6,
|
||||
}
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
make_tax_rule(
|
||||
supplier_group="All Supplier Groups",
|
||||
tax_type="Purchase",
|
||||
purchase_tax_template=purchase_template,
|
||||
priority=1,
|
||||
use_for_shopping_cart=0,
|
||||
from_date="2015-01-01",
|
||||
save=1,
|
||||
)
|
||||
|
||||
# "_Test Supplier Group" has "All Supplier Groups" as its parent — should match hierarchically
|
||||
self.assertEqual(
|
||||
get_tax_template(
|
||||
"2015-01-01",
|
||||
{
|
||||
"supplier_group": "_Test Supplier Group",
|
||||
"tax_type": "Purchase",
|
||||
"use_for_shopping_cart": 0,
|
||||
},
|
||||
),
|
||||
purchase_template,
|
||||
)
|
||||
|
||||
def test_use_for_shopping_cart_filter(self):
|
||||
city = "Test Cart City"
|
||||
# higher priority ensures this rule wins when use_for_shopping_cart is not filtered
|
||||
make_tax_rule(
|
||||
customer="_Test Customer",
|
||||
billing_city=city,
|
||||
sales_tax_template="_Test Sales Taxes and Charges Template - _TC",
|
||||
use_for_shopping_cart=0,
|
||||
priority=2,
|
||||
save=1,
|
||||
)
|
||||
make_tax_rule(
|
||||
customer="_Test Customer",
|
||||
billing_city=city,
|
||||
sales_tax_template="_Test Sales Taxes and Charges Template 1 - _TC",
|
||||
use_for_shopping_cart=1,
|
||||
priority=1,
|
||||
save=1,
|
||||
)
|
||||
|
||||
# Cart request (use_for_shopping_cart=1) filters to cart rules only
|
||||
self.assertEqual(
|
||||
get_tax_template(
|
||||
"2015-01-01",
|
||||
{"customer": "_Test Customer", "billing_city": city, "use_for_shopping_cart": 1},
|
||||
),
|
||||
"_Test Sales Taxes and Charges Template 1 - _TC",
|
||||
)
|
||||
|
||||
# Non-cart request omits use_for_shopping_cart — no filter is applied, both rules
|
||||
# are candidates; non-cart rule wins by higher priority
|
||||
self.assertEqual(
|
||||
get_tax_template(
|
||||
"2015-01-01",
|
||||
{"customer": "_Test Customer", "billing_city": city},
|
||||
),
|
||||
"_Test Sales Taxes and Charges Template - _TC",
|
||||
)
|
||||
|
||||
def test_use_for_shopping_cart_default(self):
|
||||
city = "Test Default Cart City"
|
||||
# use_for_shopping_cart not set — Check field defaults to 0
|
||||
make_tax_rule(
|
||||
customer="_Test Customer",
|
||||
billing_city=city,
|
||||
sales_tax_template="_Test Sales Taxes and Charges Template - _TC",
|
||||
use_for_shopping_cart=0, # Default is set to 1.
|
||||
save=1,
|
||||
)
|
||||
|
||||
# Non-cart request (no use_for_shopping_cart in args) matches the rule
|
||||
self.assertEqual(
|
||||
get_tax_template(
|
||||
"2015-01-01",
|
||||
{"customer": "_Test Customer", "billing_city": city},
|
||||
),
|
||||
"_Test Sales Taxes and Charges Template - _TC",
|
||||
)
|
||||
|
||||
# Cart request (use_for_shopping_cart=1) does not match — rule has default 0
|
||||
self.assertIsNone(
|
||||
get_tax_template(
|
||||
"2015-01-01",
|
||||
{"customer": "_Test Customer", "billing_city": city, "use_for_shopping_cart": 1},
|
||||
)
|
||||
)
|
||||
|
||||
def test_conflict_with_overlapping_dates(self):
|
||||
tax_rule1 = make_tax_rule(
|
||||
customer="_Test Customer",
|
||||
|
||||
@@ -700,7 +700,12 @@ def make_reverse_gl_entries(
|
||||
check_freezing_date(gl_entries[0]["posting_date"], adv_adj)
|
||||
|
||||
is_opening = any(d.get("is_opening") == "Yes" for d in gl_entries)
|
||||
validate_against_pcv(is_opening, gl_entries[0]["posting_date"], gl_entries[0]["company"])
|
||||
|
||||
# For reverse entries, use the posting_date parameter if provided and valid
|
||||
# Otherwise fall back to original posting_date
|
||||
validation_date = posting_date if posting_date else gl_entries[0]["posting_date"]
|
||||
validate_against_pcv(is_opening, validation_date, gl_entries[0]["company"])
|
||||
|
||||
if partial_cancel:
|
||||
# Partial cancel is only used by `Advance` in separate account feature.
|
||||
# Only cancel GL entries for unlinked reference using `voucher_detail_no`
|
||||
|
||||
@@ -48,6 +48,25 @@ SALES_TRANSACTION_TYPES = {
|
||||
}
|
||||
TRANSACTION_TYPES = PURCHASE_TRANSACTION_TYPES | SALES_TRANSACTION_TYPES
|
||||
|
||||
# Party-derived fields that must NOT be auto-copied by `get_mapped_doc` when the
|
||||
# source and target documents belong to different parties (e.g. Sales Order →
|
||||
# Purchase Order or inter-company Sales Invoice → Purchase Invoice).
|
||||
CROSS_PARTY_FIELD_NO_MAP = [
|
||||
"tax_category",
|
||||
"tax_id",
|
||||
"tax_withholding_category",
|
||||
"taxes_and_charges",
|
||||
"address_display",
|
||||
"contact_display",
|
||||
"contact_mobile",
|
||||
"contact_email",
|
||||
"contact_person",
|
||||
"shipping_address",
|
||||
"dispatch_address",
|
||||
"payment_terms_template",
|
||||
"language",
|
||||
]
|
||||
|
||||
|
||||
class DuplicatePartyAccountError(frappe.ValidationError):
|
||||
pass
|
||||
@@ -743,7 +762,7 @@ def set_taxes(
|
||||
args.update({"tax_type": "Purchase"})
|
||||
|
||||
if use_for_shopping_cart:
|
||||
args.update({"use_for_shopping_cart": use_for_shopping_cart})
|
||||
args.update({"use_for_shopping_cart": cint(use_for_shopping_cart)})
|
||||
|
||||
return get_tax_template(posting_date, args)
|
||||
|
||||
|
||||
@@ -406,7 +406,13 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension
|
||||
# Opening for filtered account
|
||||
data.append(totals.opening)
|
||||
|
||||
if filters.get("categorize_by") != "Categorize by Voucher (Consolidated)":
|
||||
if not filters.get("categorize_by"):
|
||||
all_entries = []
|
||||
for acc_dict in gle_map.values():
|
||||
all_entries.extend(acc_dict.entries)
|
||||
data += all_entries
|
||||
|
||||
elif filters.get("categorize_by") != "Categorize by Voucher (Consolidated)":
|
||||
for _acc, acc_dict in gle_map.items():
|
||||
# acc
|
||||
if acc_dict.entries:
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder import CustomFunction
|
||||
from frappe.utils import cint
|
||||
|
||||
|
||||
@@ -94,19 +95,35 @@ def get_data(filters):
|
||||
def get_sales_details(filters):
|
||||
item_details_map = {}
|
||||
|
||||
date_field = "s.transaction_date" if filters["based_on"] == "Sales Order" else "s.posting_date"
|
||||
if filters["based_on"] not in ("Sales Order", "Sales Invoice"):
|
||||
frappe.throw(_("Invalid value {0} for 'Based On'").format(filters["based_on"]))
|
||||
|
||||
sales_data = frappe.db.sql(
|
||||
"""
|
||||
select s.territory, s.customer, si.item_group, si.item_code, si.qty, {date_field} as last_order_date,
|
||||
DATEDIFF(CURRENT_DATE, {date_field}) as days_since_last_order
|
||||
from `tab{doctype}` s, `tab{doctype} Item` si
|
||||
where s.name = si.parent and s.docstatus = 1
|
||||
order by days_since_last_order """.format( # nosec
|
||||
date_field=date_field, doctype=filters["based_on"]
|
||||
),
|
||||
as_dict=1,
|
||||
)
|
||||
parent = frappe.qb.DocType(filters["based_on"])
|
||||
child_doctype = "Sales Order Item" if filters["based_on"] == "Sales Order" else "Sales Invoice Item"
|
||||
child = frappe.qb.DocType(child_doctype)
|
||||
|
||||
date_diff = CustomFunction("DATEDIFF", ["d1", "d2"])
|
||||
current_date = CustomFunction("CURRENT_DATE", [])
|
||||
|
||||
date_col = parent.transaction_date if filters["based_on"] == "Sales Order" else parent.posting_date
|
||||
days_since_last_order = date_diff(current_date(), date_col)
|
||||
|
||||
sales_data = (
|
||||
frappe.qb.from_(parent)
|
||||
.inner_join(child)
|
||||
.on(parent.name == child.parent)
|
||||
.select(
|
||||
parent.territory,
|
||||
parent.customer,
|
||||
child.item_group,
|
||||
child.item_code,
|
||||
child.qty,
|
||||
date_col.as_("last_order_date"),
|
||||
days_since_last_order.as_("days_since_last_order"),
|
||||
)
|
||||
.where(parent.docstatus == 1)
|
||||
.orderby(days_since_last_order)
|
||||
).run(as_dict=True)
|
||||
|
||||
for d in sales_data:
|
||||
item_details_map.setdefault((d.territory, d.item_code), d)
|
||||
|
||||
@@ -278,6 +278,7 @@ def get_balance_on(
|
||||
)
|
||||
|
||||
if party_type and party:
|
||||
frappe.has_permission(party_type, "read", party, throw=True)
|
||||
cond.append(
|
||||
f"""gle.party_type = {frappe.db.escape(party_type)} and gle.party = {frappe.db.escape(party)} """
|
||||
)
|
||||
@@ -397,15 +398,13 @@ def add_ac(args=None):
|
||||
if not args:
|
||||
args = frappe.local.form_dict
|
||||
|
||||
args.pop("ignore_permissions", None)
|
||||
frappe.has_permission("Account", "create", throw=True)
|
||||
|
||||
args.doctype = "Account"
|
||||
args = make_tree_args(**args)
|
||||
|
||||
ac = frappe.new_doc("Account")
|
||||
|
||||
if args.get("ignore_permissions"):
|
||||
ac.flags.ignore_permissions = True
|
||||
args.pop("ignore_permissions")
|
||||
|
||||
ac.update(args)
|
||||
|
||||
if not ac.parent_account:
|
||||
@@ -1934,8 +1933,9 @@ def create_payment_ledger_entry(
|
||||
ple = frappe.get_doc(entry)
|
||||
|
||||
if cancel:
|
||||
delink_original_entry(ple, partial_cancel=partial_cancel)
|
||||
if is_immutable_ledger_enabled():
|
||||
if not is_immutable_ledger_enabled():
|
||||
delink_original_entry(ple, partial_cancel=partial_cancel)
|
||||
else:
|
||||
ple.delinked = 0
|
||||
ple.posting_date = frappe.form_dict.get("posting_date") or getdate()
|
||||
|
||||
@@ -2027,6 +2027,7 @@ def delink_original_entry(pl_entry, partial_cancel=False):
|
||||
qb.update(ple)
|
||||
.set(ple.modified, now())
|
||||
.set(ple.modified_by, frappe.session.user)
|
||||
.set(ple.delinked, True)
|
||||
.where(
|
||||
(ple.company == pl_entry.company)
|
||||
& (ple.account_type == pl_entry.account_type)
|
||||
@@ -2043,9 +2044,6 @@ def delink_original_entry(pl_entry, partial_cancel=False):
|
||||
if partial_cancel:
|
||||
query = query.where(ple.voucher_detail_no == pl_entry.voucher_detail_no)
|
||||
|
||||
if not is_immutable_ledger_enabled():
|
||||
query = query.set(ple.delinked, True)
|
||||
|
||||
query.run()
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.contacts.doctype.contact.contact import get_full_name
|
||||
from frappe.core.doctype.communication.email import make
|
||||
from frappe.desk.form.load import get_attachments
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
@@ -272,12 +273,20 @@ class RequestforQuotation(BuyingController):
|
||||
supplier_doc.save()
|
||||
|
||||
def create_user(self, rfq_supplier, link):
|
||||
contact_name = None
|
||||
if rfq_supplier.contact:
|
||||
name_fields = frappe.get_value(
|
||||
"Contact", rfq_supplier.contact, ["first_name", "middle_name", "last_name"]
|
||||
)
|
||||
if name_fields:
|
||||
contact_name = get_full_name(*name_fields)
|
||||
|
||||
user = frappe.get_doc(
|
||||
{
|
||||
"doctype": "User",
|
||||
"send_welcome_email": 0,
|
||||
"email": rfq_supplier.email_id,
|
||||
"first_name": rfq_supplier.supplier_name or rfq_supplier.supplier,
|
||||
"first_name": contact_name or rfq_supplier.supplier_name or rfq_supplier.supplier,
|
||||
"user_type": "Website User",
|
||||
"redirect_url": link,
|
||||
}
|
||||
|
||||
@@ -362,19 +362,22 @@
|
||||
"fieldname": "supplier_primary_contact",
|
||||
"fieldtype": "Link",
|
||||
"label": "Supplier Primary Contact",
|
||||
"no_copy": 1,
|
||||
"options": "Contact"
|
||||
},
|
||||
{
|
||||
"fetch_from": "supplier_primary_contact.mobile_no",
|
||||
"fieldname": "mobile_no",
|
||||
"fieldtype": "Read Only",
|
||||
"label": "Mobile No"
|
||||
"label": "Mobile No",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "supplier_primary_contact.email_id",
|
||||
"fieldname": "email_id",
|
||||
"fieldtype": "Read Only",
|
||||
"label": "Email Id"
|
||||
"label": "Email Id",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_44",
|
||||
@@ -384,6 +387,7 @@
|
||||
"fieldname": "primary_address",
|
||||
"fieldtype": "Text",
|
||||
"label": "Primary Address",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -391,6 +395,7 @@
|
||||
"fieldname": "supplier_primary_address",
|
||||
"fieldtype": "Link",
|
||||
"label": "Supplier Primary Address",
|
||||
"no_copy": 1,
|
||||
"options": "Address"
|
||||
},
|
||||
{
|
||||
@@ -486,7 +491,7 @@
|
||||
"link_fieldname": "party"
|
||||
}
|
||||
],
|
||||
"modified": "2026-02-06 12:58:01.398824",
|
||||
"modified": "2026-05-29 16:52:59.441272",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Supplier",
|
||||
|
||||
@@ -7,7 +7,7 @@ from collections import defaultdict
|
||||
|
||||
import frappe
|
||||
from frappe import _, bold, qb, throw
|
||||
from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied
|
||||
from frappe.model.workflow import get_workflow_name
|
||||
from frappe.query_builder import Criterion, DocType
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.query_builder.functions import Abs, Sum
|
||||
@@ -69,6 +69,7 @@ from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
|
||||
from erpnext.stock.get_item_details import (
|
||||
_get_item_tax_template,
|
||||
_get_item_tax_template_from_item_group,
|
||||
get_bin_details,
|
||||
get_conversion_factor,
|
||||
get_item_details,
|
||||
get_item_tax_map,
|
||||
@@ -177,7 +178,7 @@ class AccountsController(TransactionBase):
|
||||
if not get_meta(self.doctype).has_field("outstanding_amount"):
|
||||
return
|
||||
|
||||
if self.get("is_return") and self.return_against and not self.get("is_pos"):
|
||||
if self.get("is_return") and self.return_against and not (self.get("is_pos") or self.get("is_paid")):
|
||||
against_voucher_outstanding = frappe.get_value(
|
||||
self.doctype, self.return_against, "outstanding_amount"
|
||||
)
|
||||
@@ -3704,6 +3705,7 @@ def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child
|
||||
child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True)
|
||||
conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor"))
|
||||
child_item.conversion_factor = flt(trans_item.get("conversion_factor")) or conversion_factor
|
||||
child_item.update(get_bin_details(child_item.item_code, child_item.warehouse, p_doc.get("company")))
|
||||
|
||||
if child_doctype in ["Purchase Order Item", "Supplier Quotation Item"]:
|
||||
# Initialized value will update in parent validation
|
||||
@@ -3813,7 +3815,9 @@ def validate_and_delete_children(parent, data, ordered_item=None) -> bool:
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"):
|
||||
def update_child_qty_rate(
|
||||
parent_doctype: str, trans_items: str, parent_doctype_name: str, child_docname: str = "items"
|
||||
):
|
||||
from erpnext.buying.doctype.supplier_quotation.supplier_quotation import get_purchased_items
|
||||
from erpnext.selling.doctype.quotation.quotation import get_ordered_items
|
||||
|
||||
@@ -3839,14 +3843,12 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
current_state = doc.get(workflow_doc.workflow_state_field)
|
||||
roles = frappe.get_roles()
|
||||
|
||||
transitions = []
|
||||
for transition in workflow_doc.transitions:
|
||||
if transition.next_state == current_state and transition.allowed in roles:
|
||||
if not is_transition_condition_satisfied(transition, doc):
|
||||
continue
|
||||
transitions.append(transition.as_dict())
|
||||
allowed = any(
|
||||
state.state == current_state and (not state.allow_edit or state.allow_edit in roles)
|
||||
for state in workflow_doc.states
|
||||
)
|
||||
|
||||
if not transitions:
|
||||
if not allowed:
|
||||
frappe.throw(
|
||||
_("You are not allowed to update as per the conditions set in {} Workflow.").format(
|
||||
get_link_to_form("Workflow", workflow)
|
||||
|
||||
@@ -209,7 +209,9 @@ def create_variant(item, args, use_template_image=False):
|
||||
variant_attributes = []
|
||||
|
||||
for d in template.attributes:
|
||||
variant_attributes.append({"attribute": d.attribute, "attribute_value": args.get(d.attribute)})
|
||||
attribute_value = args.get(_(d.attribute)) or args.get(d.attribute)
|
||||
if attribute_value:
|
||||
variant_attributes.append({"attribute": d.attribute, "attribute_value": attribute_value})
|
||||
|
||||
variant.set("attributes", variant_attributes)
|
||||
copy_attributes_to_variant(template, variant)
|
||||
@@ -228,6 +230,12 @@ def enqueue_multiple_variant_creation(item, args, use_template_image=False):
|
||||
# There can be innumerable attribute combinations, enqueue
|
||||
if isinstance(args, str):
|
||||
variants = json.loads(args)
|
||||
else:
|
||||
variants = args
|
||||
variants = {key: values for key, values in variants.items() if values}
|
||||
if not variants:
|
||||
frappe.throw(_("Please select at least one attribute value"))
|
||||
|
||||
total_variants = 1
|
||||
for key in variants:
|
||||
total_variants *= len(variants[key])
|
||||
@@ -251,6 +259,7 @@ def create_multiple_variants(item, args, use_template_image=False):
|
||||
count = 0
|
||||
if isinstance(args, str):
|
||||
args = json.loads(args)
|
||||
args = {key: values for key, values in args.items() if values}
|
||||
|
||||
template_item = frappe.get_doc("Item", item)
|
||||
args_set = generate_keyed_value_combinations(args)
|
||||
@@ -285,6 +294,9 @@ def generate_keyed_value_combinations(args):
|
||||
|
||||
"""
|
||||
# Return empty list if empty
|
||||
if not args:
|
||||
return []
|
||||
args = {key: values for key, values in args.items() if values}
|
||||
if not args:
|
||||
return []
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ from pypika import Order
|
||||
import erpnext
|
||||
from erpnext.accounts.utils import build_qb_match_conditions
|
||||
from erpnext.stock.get_item_details import _get_item_tax_template
|
||||
from erpnext.stock.utils import get_combine_datetime
|
||||
|
||||
|
||||
# searches for active employees
|
||||
@@ -369,10 +370,14 @@ def get_delivery_notes_to_be_billed(
|
||||
.where((DeliveryNote.docstatus == 1) & (DeliveryNote.is_return == 0) & (DeliveryNote.per_billed > 0))
|
||||
)
|
||||
|
||||
query = frappe.qb.get_query(
|
||||
"Delivery Note",
|
||||
fields=fields,
|
||||
filters=filters,
|
||||
)
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(DeliveryNote)
|
||||
.select(*[DeliveryNote[f] for f in fields])
|
||||
.where(
|
||||
query.where(
|
||||
(DeliveryNote.docstatus == 1)
|
||||
& (DeliveryNote.status.notin(["Stopped", "Closed"]))
|
||||
& (DeliveryNote[searchfield].like(f"%{txt}%"))
|
||||
@@ -386,12 +391,11 @@ def get_delivery_notes_to_be_billed(
|
||||
)
|
||||
)
|
||||
)
|
||||
.orderby(DeliveryNote[searchfield], order=Order.asc)
|
||||
.limit(page_len)
|
||||
.offset(start)
|
||||
)
|
||||
if filters and isinstance(filters, dict):
|
||||
for key, value in filters.items():
|
||||
query = query.where(DeliveryNote[key] == value)
|
||||
|
||||
query = query.orderby(DeliveryNote[searchfield], order=Order.asc).limit(page_len).offset(start)
|
||||
return query.run(as_dict=as_dict)
|
||||
|
||||
|
||||
@@ -476,6 +480,13 @@ def get_batches_from_stock_ledger_entries(searchfields, txt, filters, start=0, p
|
||||
.limit(page_len)
|
||||
)
|
||||
|
||||
if not filters.get("is_inward"):
|
||||
if filters.get("posting_date") and filters.get("posting_time"):
|
||||
query = query.where(
|
||||
stock_ledger_entry.posting_datetime
|
||||
<= get_combine_datetime(filters.get("posting_date"), filters.get("posting_time"))
|
||||
)
|
||||
|
||||
if not filters.get("include_expired_batches"):
|
||||
query = query.where((batch_table.expiry_date >= expiry_date) | (batch_table.expiry_date.isnull()))
|
||||
|
||||
@@ -529,6 +540,13 @@ def get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start=0
|
||||
.limit(page_len)
|
||||
)
|
||||
|
||||
if not filters.get("is_inward"):
|
||||
if filters.get("posting_date") and filters.get("posting_time"):
|
||||
bundle_query = bundle_query.where(
|
||||
stock_ledger_entry.posting_datetime
|
||||
<= get_combine_datetime(filters.get("posting_date"), filters.get("posting_time"))
|
||||
)
|
||||
|
||||
if not filters.get("include_expired_batches"):
|
||||
bundle_query = bundle_query.where(
|
||||
(batch_table.expiry_date >= expiry_date) | (batch_table.expiry_date.isnull())
|
||||
|
||||
@@ -53,6 +53,7 @@ class SellingController(StockController):
|
||||
self.validate_for_duplicate_items()
|
||||
self.validate_target_warehouse()
|
||||
self.validate_auto_repeat_subscription_dates()
|
||||
self.validate_sample_retention_warehouse()
|
||||
for table_field in ["items", "packed_items"]:
|
||||
if self.get(table_field):
|
||||
self.set_serial_and_batch_bundle(table_field)
|
||||
@@ -874,6 +875,26 @@ class SellingController(StockController):
|
||||
|
||||
validate_item_type(self, "is_sales_item", "sales")
|
||||
|
||||
def validate_sample_retention_warehouse(self):
|
||||
if self.get("is_return"):
|
||||
return
|
||||
|
||||
sample_retention_warehouse = frappe.db.get_single_value(
|
||||
"Stock Settings", "sample_retention_warehouse"
|
||||
)
|
||||
if not sample_retention_warehouse:
|
||||
return
|
||||
|
||||
items = self.get("items") + (self.get("packed_items"))
|
||||
for item in items:
|
||||
if item.get("warehouse") == sample_retention_warehouse:
|
||||
frappe.throw(
|
||||
_("Row {0}: Cannot sell item {1} from Sample Retention Warehouse {2}").format(
|
||||
item.idx, frappe.bold(item.item_code), frappe.bold(sample_retention_warehouse)
|
||||
),
|
||||
title=_("Not Allowed"),
|
||||
)
|
||||
|
||||
def update_stock_reservation_entries(self) -> None:
|
||||
"""Updates Delivered Qty in Stock Reservation Entries."""
|
||||
|
||||
|
||||
@@ -240,10 +240,10 @@ class StatusUpdater(Document):
|
||||
|
||||
# get unique transactions to update
|
||||
for d in self.get_all_children():
|
||||
if hasattr(d, "qty") and d.qty < 0 and not self.get("is_return"):
|
||||
if hasattr(d, "qty") and flt(d.qty) < 0 and not self.get("is_return"):
|
||||
frappe.throw(_("For an item {0}, quantity must be positive number").format(d.item_code))
|
||||
|
||||
if hasattr(d, "qty") and d.qty > 0 and self.get("is_return"):
|
||||
if hasattr(d, "qty") and flt(d.qty) > 0 and self.get("is_return"):
|
||||
frappe.throw(_("For an item {0}, quantity must be negative number").format(d.item_code))
|
||||
|
||||
if not frappe.db.get_single_value("Selling Settings", "allow_negative_rates_for_items"):
|
||||
|
||||
@@ -1680,7 +1680,7 @@ def repost_required_for_queue(doc: StockController) -> bool:
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def check_item_quality_inspection(doctype, items):
|
||||
def check_item_quality_inspection(doctype: str, docstatus: str | int, items: str | list[dict]):
|
||||
if isinstance(items, str):
|
||||
items = json.loads(items)
|
||||
|
||||
@@ -1692,13 +1692,30 @@ def check_item_quality_inspection(doctype, items):
|
||||
"Delivery Note": "inspection_required_before_delivery",
|
||||
}
|
||||
|
||||
items_to_remove = []
|
||||
for item in items:
|
||||
if not frappe.db.get_value("Item", item.get("item_code"), inspection_fieldname_map.get(doctype)):
|
||||
items_to_remove.append(item)
|
||||
items = [item for item in items if item not in items_to_remove]
|
||||
inspection_fieldname = inspection_fieldname_map.get(doctype)
|
||||
if inspection_fieldname is None:
|
||||
return []
|
||||
|
||||
return items
|
||||
allow_after_transaction = cint(docstatus) == 1 and frappe.get_single_value(
|
||||
"Stock Settings", "allow_to_make_quality_inspection_after_purchase_or_delivery"
|
||||
)
|
||||
|
||||
if allow_after_transaction:
|
||||
return items
|
||||
|
||||
item_codes = list({item.get("item_code") for item in items})
|
||||
|
||||
Item = frappe.qb.DocType("Item")
|
||||
results = (
|
||||
frappe.qb.from_(Item)
|
||||
.select(Item.name)
|
||||
.where((Item.name.isin(item_codes)) & (Item[inspection_fieldname] == 1))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
inspection_required_items = {row.name for row in results}
|
||||
|
||||
return [item for item in items if item.get("item_code") in inspection_required_items]
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -3,7 +3,11 @@ import unittest
|
||||
|
||||
import frappe
|
||||
|
||||
from erpnext.controllers.item_variant import copy_attributes_to_variant, make_variant_item_code
|
||||
from erpnext.controllers.item_variant import (
|
||||
copy_attributes_to_variant,
|
||||
generate_keyed_value_combinations,
|
||||
make_variant_item_code,
|
||||
)
|
||||
from erpnext.stock.doctype.item.test_item import set_item_variant_settings
|
||||
from erpnext.stock.doctype.quality_inspection.test_quality_inspection import (
|
||||
create_quality_inspection_parameter,
|
||||
@@ -17,6 +21,19 @@ class TestItemVariant(unittest.TestCase):
|
||||
variant = make_item_variant()
|
||||
self.assertEqual(variant.get("quality_inspection_template"), "_Test QC Template")
|
||||
|
||||
def test_generate_keyed_value_combinations_ignores_empty_attributes(self):
|
||||
combinations = generate_keyed_value_combinations(
|
||||
{"Test Colour": ["Red", "Blue"], "Test Size": ["Small", "Large"], "Test Fit": []}
|
||||
)
|
||||
|
||||
self.assertEqual(len(combinations), 4)
|
||||
self.assertNotIn("Test Fit", combinations[0])
|
||||
|
||||
single_attribute_combinations = generate_keyed_value_combinations(
|
||||
{"Test Colour": ["Red", "Blue"], "Test Size": []}
|
||||
)
|
||||
self.assertEqual(single_attribute_combinations, [{"Test Colour": "Red"}, {"Test Colour": "Blue"}])
|
||||
|
||||
|
||||
def create_variant_with_tables(item, args):
|
||||
if isinstance(args, str):
|
||||
|
||||
@@ -178,13 +178,16 @@ def get_list_for_transactions(
|
||||
|
||||
|
||||
def rfq_transaction_list(parties_doctype, doctype, parties, limit_start, limit_page_length):
|
||||
data = frappe.db.sql(
|
||||
"""select distinct parent as name, supplier from `tab{doctype}`
|
||||
where supplier = '{supplier}' and docstatus=1 order by modified desc limit {start}, {len}""".format(
|
||||
doctype=parties_doctype, supplier=parties[0], start=limit_start, len=limit_page_length
|
||||
),
|
||||
as_dict=1,
|
||||
)
|
||||
party = frappe.qb.DocType(parties_doctype)
|
||||
data = (
|
||||
frappe.qb.from_(party)
|
||||
.select(party.parent.as_("name"), party.supplier)
|
||||
.distinct()
|
||||
.where((party.supplier == party[0]) & (party.docstatus == 1))
|
||||
.orderby(party.creation, order=frappe.qb.desc)
|
||||
.limit(limit_page_length)
|
||||
.offset(limit_start)
|
||||
).run(as_dict=True)
|
||||
|
||||
return post_process(doctype, data)
|
||||
|
||||
|
||||
@@ -32,20 +32,16 @@ def create_custom_fields_for_frappe_crm():
|
||||
@frappe.whitelist()
|
||||
def create_prospect_against_crm_deal():
|
||||
doc = frappe.form_dict
|
||||
prospect = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Prospect",
|
||||
"company_name": doc.organization or doc.lead_name,
|
||||
"no_of_employees": doc.no_of_employees,
|
||||
"prospect_owner": doc.deal_owner,
|
||||
"company": doc.erpnext_company,
|
||||
"crm_deal": doc.crm_deal,
|
||||
"territory": doc.territory,
|
||||
"industry": doc.industry,
|
||||
"website": doc.website,
|
||||
"annual_revenue": doc.annual_revenue,
|
||||
}
|
||||
)
|
||||
prospect = frappe.new_doc("Prospect")
|
||||
prospect.company_name = doc.organization or doc.lead_name
|
||||
prospect.no_of_employees = doc.no_of_employees
|
||||
prospect.prospect_owner = doc.deal_owner
|
||||
prospect.company = doc.erpnext_company
|
||||
prospect.crm_deal = doc.crm_deal
|
||||
prospect.territory = doc.territory
|
||||
prospect.industry = doc.industry
|
||||
prospect.website = doc.website
|
||||
prospect.annual_revenue = doc.annual_revenue
|
||||
|
||||
try:
|
||||
prospect_name = frappe.db.get_value("Prospect", {"company_name": prospect.company_name})
|
||||
@@ -151,6 +147,18 @@ def contact_exists(email, mobile_no):
|
||||
return False
|
||||
|
||||
|
||||
CUSTOMER_ALLOWED_FIELDS = {
|
||||
"customer_name",
|
||||
"customer_group",
|
||||
"customer_type",
|
||||
"territory",
|
||||
"default_currency",
|
||||
"industry",
|
||||
"website",
|
||||
"crm_deal",
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_customer(customer_data=None):
|
||||
if not customer_data:
|
||||
@@ -159,9 +167,11 @@ def create_customer(customer_data=None):
|
||||
try:
|
||||
customer_name = frappe.db.exists("Customer", {"customer_name": customer_data.get("customer_name")})
|
||||
if not customer_name:
|
||||
customer = frappe.get_doc({"doctype": "Customer", **customer_data}).insert(
|
||||
ignore_permissions=True
|
||||
)
|
||||
customer = frappe.new_doc("Customer")
|
||||
for field in CUSTOMER_ALLOWED_FIELDS:
|
||||
if customer_data.get(field) is not None:
|
||||
customer.set(field, customer_data.get(field))
|
||||
customer.insert(ignore_permissions=True)
|
||||
customer_name = customer.name
|
||||
|
||||
contacts = json.loads(customer_data.get("contacts"))
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
{
|
||||
"custom_fields": [
|
||||
{
|
||||
"_assign": null,
|
||||
"_comments": null,
|
||||
"_liked_by": null,
|
||||
"_user_tags": null,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"collapsible_depends_on": null,
|
||||
"columns": 0,
|
||||
"creation": "2019-12-02 11:00:03.432994",
|
||||
"default": null,
|
||||
"depends_on": null,
|
||||
"description": null,
|
||||
"docstatus": 0,
|
||||
"dt": "Contact",
|
||||
"fetch_from": null,
|
||||
"fetch_if_empty": 0,
|
||||
"fieldname": "is_billing_contact",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"idx": 27,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"insert_after": "is_primary_contact",
|
||||
"label": "Is Billing Contact",
|
||||
"length": 0,
|
||||
"modified": "2019-12-02 11:00:03.432994",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Contact-is_billing_contact",
|
||||
"no_copy": 0,
|
||||
"options": null,
|
||||
"owner": "Administrator",
|
||||
"parent": null,
|
||||
"parentfield": null,
|
||||
"parenttype": null,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": null,
|
||||
"read_only": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0,
|
||||
"width": null
|
||||
}
|
||||
],
|
||||
"custom_perms": [],
|
||||
"doctype": "Contact",
|
||||
"property_setters": [],
|
||||
"sync_on_migrate": 1
|
||||
}
|
||||
@@ -438,7 +438,7 @@ frappe.ui.form.on("BOM", {
|
||||
},
|
||||
|
||||
routing(frm) {
|
||||
if (frm.doc.routing) {
|
||||
if (frm.doc.routing && frm.doc.with_operations && !frm.doc.operations) {
|
||||
frappe.call({
|
||||
doc: frm.doc,
|
||||
method: "get_routing",
|
||||
|
||||
@@ -1367,18 +1367,71 @@ def add_non_stock_items_cost(stock_entry, work_order, expense_account):
|
||||
|
||||
|
||||
def add_operations_cost(stock_entry, work_order=None, expense_account=None):
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry import get_operating_cost_per_unit
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry import (
|
||||
get_consumed_operating_cost,
|
||||
get_operating_cost_per_unit,
|
||||
)
|
||||
|
||||
operating_cost_per_unit = get_operating_cost_per_unit(work_order, stock_entry.bom_no)
|
||||
|
||||
if operating_cost_per_unit:
|
||||
stock_entry.append(
|
||||
"additional_costs",
|
||||
{
|
||||
def append_operating_cost(amount, operation=None, qty=None):
|
||||
if amount:
|
||||
row = {
|
||||
"expense_account": expense_account,
|
||||
"description": _("Operating Cost as per Work Order / BOM"),
|
||||
"amount": operating_cost_per_unit * flt(stock_entry.fg_completed_qty),
|
||||
},
|
||||
"amount": flt(
|
||||
amount,
|
||||
frappe.get_precision("Landed Cost Taxes and Charges", "amount"),
|
||||
),
|
||||
"has_operating_cost": 1,
|
||||
}
|
||||
if operation:
|
||||
row["operation_id"] = operation.name
|
||||
if qty is not None:
|
||||
row["qty"] = qty
|
||||
stock_entry.append(
|
||||
"additional_costs",
|
||||
row,
|
||||
)
|
||||
|
||||
if (
|
||||
work_order
|
||||
and stock_entry.bom_no
|
||||
and frappe.db.get_single_value("Manufacturing Settings", "set_op_cost_and_scrap_from_sub_assemblies")
|
||||
and work_order.get("use_multi_level_bom")
|
||||
):
|
||||
operating_cost_per_unit = get_operating_cost_per_unit(work_order, stock_entry.bom_no)
|
||||
append_operating_cost(
|
||||
operating_cost_per_unit * flt(stock_entry.fg_completed_qty),
|
||||
qty=flt(stock_entry.fg_completed_qty),
|
||||
)
|
||||
elif work_order and work_order.get("operations"):
|
||||
for operation in work_order.get("operations"):
|
||||
qty = flt(stock_entry.fg_completed_qty)
|
||||
amount = 0
|
||||
|
||||
if flt(operation.completed_qty):
|
||||
consumed_cost = get_consumed_operating_cost(
|
||||
work_order.name, stock_entry.bom_no, operation.name
|
||||
)
|
||||
remaining_cost = flt(
|
||||
flt(operation.actual_operating_cost) - flt(consumed_cost.get("consumed_cost")),
|
||||
operation.precision("actual_operating_cost"),
|
||||
)
|
||||
remaining_qty = flt(operation.completed_qty) - flt(consumed_cost.get("consumed_qty"))
|
||||
|
||||
if remaining_cost <= 0 or remaining_qty <= 0:
|
||||
continue
|
||||
|
||||
qty = min(remaining_qty, flt(stock_entry.fg_completed_qty))
|
||||
amount = remaining_cost / remaining_qty * qty
|
||||
elif work_order.qty:
|
||||
amount = flt(operation.planned_operating_cost) / flt(work_order.qty) * qty
|
||||
|
||||
append_operating_cost(amount, operation=operation, qty=qty)
|
||||
else:
|
||||
operating_cost_per_unit = get_operating_cost_per_unit(work_order, stock_entry.bom_no)
|
||||
append_operating_cost(
|
||||
operating_cost_per_unit * flt(stock_entry.fg_completed_qty),
|
||||
qty=flt(stock_entry.fg_completed_qty),
|
||||
)
|
||||
|
||||
if work_order and work_order.additional_operating_cost and work_order.qty:
|
||||
|
||||
@@ -126,11 +126,13 @@
|
||||
"label": "Image"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fetch_from": "operation.batch_size",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "batch_size",
|
||||
"fieldtype": "Int",
|
||||
"label": "Batch Size"
|
||||
"fieldtype": "Float",
|
||||
"label": "Batch Size",
|
||||
"non_negative": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.parenttype == \"Routing\" || !parent.routing",
|
||||
@@ -196,13 +198,14 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-02-17 15:33:28.495850",
|
||||
"modified": "2026-05-27 12:09:44.797434",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "BOM Operation",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ class BOMOperation(Document):
|
||||
base_cost_per_unit: DF.Float
|
||||
base_hour_rate: DF.Currency
|
||||
base_operating_cost: DF.Currency
|
||||
batch_size: DF.Int
|
||||
batch_size: DF.Float
|
||||
cost_per_unit: DF.Float
|
||||
description: DF.TextEditor | None
|
||||
fixed_time: DF.Check
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_bulk_edit": 1,
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2018-07-09 17:23:29.518745",
|
||||
"doctype": "DocType",
|
||||
@@ -135,6 +136,7 @@
|
||||
"fieldname": "wip_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "WIP Warehouse",
|
||||
"link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]",
|
||||
"options": "Warehouse",
|
||||
"reqd": 1
|
||||
},
|
||||
@@ -511,7 +513,7 @@
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-08-04 15:47:54.514290",
|
||||
"modified": "2026-05-12 12:17:17.750857",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Job Card",
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import Literal
|
||||
import frappe
|
||||
from frappe.test_runner import make_test_records
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import random_string
|
||||
from frappe.utils import flt, random_string
|
||||
from frappe.utils.data import add_to_date, now, today
|
||||
|
||||
from erpnext.manufacturing.doctype.job_card.job_card import (
|
||||
@@ -697,6 +697,403 @@ class TestJobCard(FrappeTestCase):
|
||||
self.assertEqual(wo_doc.process_loss_qty, 2)
|
||||
self.assertEqual(wo_doc.status, "Completed")
|
||||
|
||||
def test_op_cost_calculation(self):
|
||||
from erpnext.manufacturing.doctype.routing.test_routing import (
|
||||
create_routing,
|
||||
setup_bom,
|
||||
setup_operations,
|
||||
)
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import make_job_card
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||
make_stock_entry as make_stock_entry_for_wo,
|
||||
)
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
suffix = random_string(5)
|
||||
workstation = make_workstation(
|
||||
workstation_name=f"Test Workstation Z {suffix}", hour_rate_rent=240, hour_rate_labour=0
|
||||
)
|
||||
workstation.update(
|
||||
{
|
||||
"hour_rate_rent": 240,
|
||||
"hour_rate_labour": 0,
|
||||
"hour_rate_electricity": 0,
|
||||
"hour_rate_consumable": 0,
|
||||
}
|
||||
)
|
||||
workstation.save()
|
||||
operations = [
|
||||
{
|
||||
"operation": f"Test Operation A1 {suffix}",
|
||||
"workstation": workstation.name,
|
||||
"time_in_mins": 30,
|
||||
},
|
||||
]
|
||||
|
||||
warehouse = create_warehouse(f"Test Warehouse 123 for Job Card {suffix}")
|
||||
setup_operations(operations)
|
||||
|
||||
item_code = f"Test Job Card Process Qty Item {suffix}"
|
||||
for item in [item_code, item_code + "RM 1", item_code + "RM 2"]:
|
||||
if not frappe.db.exists("Item", item):
|
||||
make_item(
|
||||
item,
|
||||
{
|
||||
"item_name": item,
|
||||
"stock_uom": "Nos",
|
||||
"is_stock_item": 1,
|
||||
},
|
||||
)
|
||||
|
||||
routing_doc = create_routing(routing_name="Testing Route", operations=operations)
|
||||
bom_doc = setup_bom(
|
||||
item_code=item_code,
|
||||
routing=routing_doc.name,
|
||||
raw_materials=[item_code + "RM 1", item_code + "RM 2"],
|
||||
source_warehouse=warehouse,
|
||||
)
|
||||
|
||||
for row in bom_doc.items:
|
||||
make_stock_entry(
|
||||
item_code=row.item_code,
|
||||
target=row.source_warehouse,
|
||||
qty=10,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
wo_doc = make_wo_order_test_record(
|
||||
production_item=item_code,
|
||||
bom_no=bom_doc.name,
|
||||
qty=10,
|
||||
skip_transfer=1,
|
||||
wip_warehouse=warehouse,
|
||||
source_warehouse=warehouse,
|
||||
)
|
||||
|
||||
first_job_card = frappe.get_all(
|
||||
"Job Card",
|
||||
filters={"work_order": wo_doc.name, "sequence_id": 1},
|
||||
fields=["name"],
|
||||
order_by="sequence_id",
|
||||
limit=1,
|
||||
)[0].name
|
||||
|
||||
jc = frappe.get_doc("Job Card", first_job_card)
|
||||
from_time = "2025-01-01 09:00:00"
|
||||
for _ in jc.scheduled_time_logs:
|
||||
jc.append(
|
||||
"time_logs",
|
||||
{
|
||||
"from_time": from_time,
|
||||
"to_time": add_to_date(from_time, minutes=1),
|
||||
"completed_qty": 4,
|
||||
},
|
||||
)
|
||||
jc.for_quantity = 4
|
||||
jc.save()
|
||||
jc.submit()
|
||||
|
||||
s1 = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 4))
|
||||
s1.submit()
|
||||
|
||||
wo_doc.reload()
|
||||
precision = s1.additional_costs[0].precision("amount")
|
||||
self.assertEqual(
|
||||
flt(s1.additional_costs[0].amount, precision),
|
||||
flt(wo_doc.operations[0].actual_operating_cost, precision),
|
||||
)
|
||||
|
||||
make_job_card(
|
||||
wo_doc.name,
|
||||
[
|
||||
{
|
||||
"name": wo_doc.operations[0].name,
|
||||
"operation": operations[0]["operation"],
|
||||
"workstation": wo_doc.operations[0].workstation,
|
||||
"qty": 6,
|
||||
"pending_qty": 6,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
job_card = frappe.get_last_doc("Job Card", {"work_order": wo_doc.name})
|
||||
from_time = "2025-01-01 10:00:00"
|
||||
job_card.append(
|
||||
"time_logs",
|
||||
{
|
||||
"from_time": from_time,
|
||||
"to_time": add_to_date(from_time, minutes=2),
|
||||
"completed_qty": 6,
|
||||
},
|
||||
)
|
||||
job_card.for_quantity = 6
|
||||
job_card.save()
|
||||
job_card.submit()
|
||||
|
||||
s2 = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 6))
|
||||
wo_doc.reload()
|
||||
precision = s2.additional_costs[0].precision("amount")
|
||||
self.assertEqual(
|
||||
flt(s2.additional_costs[0].amount, precision),
|
||||
flt(wo_doc.operations[0].actual_operating_cost - s1.additional_costs[0].amount, precision),
|
||||
)
|
||||
|
||||
@change_settings("Manufacturing Settings", {"overproduction_percentage_for_work_order": 100})
|
||||
def test_operating_cost_with_overproduction(self):
|
||||
from erpnext.manufacturing.doctype.routing.test_routing import (
|
||||
create_routing,
|
||||
setup_bom,
|
||||
setup_operations,
|
||||
)
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import make_job_card
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||
make_stock_entry as make_stock_entry_for_wo,
|
||||
)
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
suffix = random_string(5)
|
||||
workstation = make_workstation(
|
||||
workstation_name=f"Test Workstation for Overproduction {suffix}",
|
||||
hour_rate_rent=10,
|
||||
hour_rate_labour=10,
|
||||
)
|
||||
workstation.update(
|
||||
{
|
||||
"hour_rate_rent": 10,
|
||||
"hour_rate_labour": 10,
|
||||
"hour_rate_electricity": 0,
|
||||
"hour_rate_consumable": 0,
|
||||
}
|
||||
)
|
||||
workstation.save()
|
||||
operations = [
|
||||
{"operation": f"Test Operation 1 {suffix}", "workstation": workstation.name, "time_in_mins": 30},
|
||||
{"operation": f"Test Operation 2 {suffix}", "workstation": workstation.name, "time_in_mins": 30},
|
||||
]
|
||||
warehouse = create_warehouse(f"Test Warehouse for Overproduction {suffix}")
|
||||
setup_operations(operations)
|
||||
|
||||
fg = make_item(f"Test FG for Overproduction {suffix}", {"stock_uom": "Nos", "is_stock_item": 1})
|
||||
rm = make_item(f"Test RM for Overproduction {suffix}", {"stock_uom": "Nos", "is_stock_item": 1})
|
||||
|
||||
routing_doc = create_routing(routing_name=f"Testing Route {suffix}", operations=operations)
|
||||
bom_doc = setup_bom(
|
||||
item_code=fg.name,
|
||||
routing=routing_doc.name,
|
||||
raw_materials=[rm.name],
|
||||
source_warehouse=warehouse,
|
||||
)
|
||||
|
||||
for row in bom_doc.items:
|
||||
make_stock_entry(
|
||||
item_code=row.item_code,
|
||||
target=row.source_warehouse,
|
||||
qty=100,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
wo_doc = make_wo_order_test_record(
|
||||
production_item=fg.name,
|
||||
bom_no=bom_doc.name,
|
||||
qty=10,
|
||||
skip_transfer=1,
|
||||
source_warehouse=warehouse,
|
||||
)
|
||||
|
||||
first_operation = frappe.get_all(
|
||||
"Job Card",
|
||||
filters={"work_order": wo_doc.name, "sequence_id": 1},
|
||||
fields=["name"],
|
||||
order_by="sequence_id",
|
||||
limit=1,
|
||||
)[0].name
|
||||
|
||||
jc = frappe.get_doc("Job Card", first_operation)
|
||||
from_time = "2025-01-02 09:00:00"
|
||||
for _ in jc.scheduled_time_logs:
|
||||
jc.append(
|
||||
"time_logs",
|
||||
{
|
||||
"from_time": from_time,
|
||||
"to_time": add_to_date(from_time, days=1),
|
||||
"completed_qty": 4,
|
||||
},
|
||||
)
|
||||
jc.for_quantity = 4
|
||||
jc.save()
|
||||
jc.submit()
|
||||
|
||||
second_operation = frappe.get_all(
|
||||
"Job Card",
|
||||
filters={"work_order": wo_doc.name, "sequence_id": 2},
|
||||
fields=["name"],
|
||||
order_by="sequence_id",
|
||||
limit=1,
|
||||
)[0].name
|
||||
|
||||
jc = frappe.get_doc("Job Card", second_operation)
|
||||
from_time = "2025-01-05 09:00:00"
|
||||
for _ in jc.scheduled_time_logs:
|
||||
jc.append(
|
||||
"time_logs",
|
||||
{
|
||||
"from_time": from_time,
|
||||
"to_time": add_to_date(from_time, days=2),
|
||||
"completed_qty": 4,
|
||||
},
|
||||
)
|
||||
jc.for_quantity = 4
|
||||
jc.save()
|
||||
jc.submit()
|
||||
|
||||
s = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 6)) # overproduction
|
||||
s.submit()
|
||||
|
||||
def assert_operating_costs(stock_entry, qty, previous_entries):
|
||||
wo_doc.reload()
|
||||
for idx, operation in enumerate(wo_doc.operations):
|
||||
consumed_cost = sum(
|
||||
entry.additional_costs[idx].amount for entry in previous_entries if entry.docstatus == 1
|
||||
)
|
||||
consumed_qty = sum(
|
||||
entry.additional_costs[idx].qty for entry in previous_entries if entry.docstatus == 1
|
||||
)
|
||||
remaining_cost = operation.actual_operating_cost - consumed_cost
|
||||
remaining_qty = operation.completed_qty - consumed_qty
|
||||
precision = stock_entry.additional_costs[idx].precision("amount")
|
||||
expected_cost = flt(remaining_cost / remaining_qty * min(remaining_qty, qty), precision)
|
||||
|
||||
self.assertEqual(flt(stock_entry.additional_costs[idx].amount, precision), expected_cost)
|
||||
|
||||
assert_operating_costs(s, 6, [])
|
||||
|
||||
make_job_card(
|
||||
wo_doc.name,
|
||||
[
|
||||
{
|
||||
"name": wo_doc.operations[0].name,
|
||||
"operation": operations[0]["operation"],
|
||||
"workstation": wo_doc.operations[0].workstation,
|
||||
"qty": 2,
|
||||
"pending_qty": 2,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
job_card = frappe.get_last_doc("Job Card", {"work_order": wo_doc.name})
|
||||
from_time = "2025-01-09 09:00:00"
|
||||
job_card.append(
|
||||
"time_logs",
|
||||
{
|
||||
"from_time": from_time,
|
||||
"to_time": add_to_date(from_time, days=1),
|
||||
"completed_qty": 2,
|
||||
},
|
||||
)
|
||||
job_card.for_quantity = 2
|
||||
job_card.save()
|
||||
job_card.submit()
|
||||
|
||||
make_job_card(
|
||||
wo_doc.name,
|
||||
[
|
||||
{
|
||||
"name": wo_doc.operations[1].name,
|
||||
"operation": operations[1]["operation"],
|
||||
"workstation": wo_doc.operations[1].workstation,
|
||||
"qty": 2,
|
||||
"pending_qty": 2,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
job_card = frappe.get_last_doc("Job Card", {"work_order": wo_doc.name})
|
||||
from_time = "2025-01-12 09:00:00"
|
||||
job_card.append(
|
||||
"time_logs",
|
||||
{
|
||||
"from_time": from_time,
|
||||
"to_time": add_to_date(from_time, days=2),
|
||||
"completed_qty": 2,
|
||||
},
|
||||
)
|
||||
job_card.for_quantity = 2
|
||||
job_card.save()
|
||||
job_card.submit()
|
||||
|
||||
s2 = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 1))
|
||||
s2.submit()
|
||||
|
||||
assert_operating_costs(s2, 1, [s])
|
||||
|
||||
make_job_card(
|
||||
wo_doc.name,
|
||||
[
|
||||
{
|
||||
"name": wo_doc.operations[0].name,
|
||||
"operation": operations[0]["operation"],
|
||||
"workstation": wo_doc.operations[0].workstation,
|
||||
"qty": 2,
|
||||
"pending_qty": 2,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
job_card = frappe.get_last_doc("Job Card", {"work_order": wo_doc.name})
|
||||
from_time = "2025-01-16 09:00:00"
|
||||
job_card.append(
|
||||
"time_logs",
|
||||
{
|
||||
"from_time": from_time,
|
||||
"to_time": add_to_date(from_time, days=1),
|
||||
"completed_qty": 2,
|
||||
},
|
||||
)
|
||||
job_card.for_quantity = 2
|
||||
job_card.save()
|
||||
job_card.submit()
|
||||
|
||||
make_job_card(
|
||||
wo_doc.name,
|
||||
[
|
||||
{
|
||||
"name": wo_doc.operations[1].name,
|
||||
"operation": operations[1]["operation"],
|
||||
"workstation": wo_doc.operations[1].workstation,
|
||||
"qty": 2,
|
||||
"pending_qty": 2,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
job_card = frappe.get_last_doc("Job Card", {"work_order": wo_doc.name})
|
||||
from_time = "2025-01-19 09:00:00"
|
||||
job_card.append(
|
||||
"time_logs",
|
||||
{
|
||||
"from_time": from_time,
|
||||
"to_time": add_to_date(from_time, days=2),
|
||||
"completed_qty": 2,
|
||||
},
|
||||
)
|
||||
job_card.for_quantity = 2
|
||||
job_card.save()
|
||||
job_card.submit()
|
||||
|
||||
s3 = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 2))
|
||||
s3.submit()
|
||||
|
||||
assert_operating_costs(s3, 2, [s, s2])
|
||||
|
||||
s2.cancel()
|
||||
|
||||
s4 = frappe.get_doc(make_stock_entry_for_wo(wo_doc.name, "Manufacture", 3))
|
||||
s4.submit()
|
||||
|
||||
assert_operating_costs(s4, 3, [s, s3])
|
||||
|
||||
|
||||
def create_bom_with_multiple_operations():
|
||||
"Create a BOM with multiple operations and Material Transfer against Job Card"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_bulk_edit": 1,
|
||||
"creation": "2018-07-09 17:20:44.737289",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
@@ -33,6 +34,7 @@
|
||||
"ignore_user_permissions": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Source Warehouse",
|
||||
"link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
@@ -105,7 +107,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-22 18:50:00.003444",
|
||||
"modified": "2026-05-12 12:22:18.506904",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Job Card Item",
|
||||
@@ -115,4 +117,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"min_order_qty",
|
||||
"section_break_8",
|
||||
"sales_order",
|
||||
"main_item_code",
|
||||
"bin_qty_section",
|
||||
"actual_qty",
|
||||
"requested_qty",
|
||||
@@ -114,6 +115,14 @@
|
||||
"options": "Sales Order",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "main_item_code",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Main Item Code",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "requested_qty",
|
||||
"fieldtype": "Float",
|
||||
@@ -213,4 +222,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ class MaterialRequestPlanItem(Document):
|
||||
from_warehouse: DF.Link | None
|
||||
item_code: DF.Link
|
||||
item_name: DF.Data | None
|
||||
main_item_code: DF.Data | None
|
||||
material_request_type: DF.Literal[
|
||||
"", "Purchase", "Material Transfer", "Material Issue", "Manufacture", "Customer Provided"
|
||||
]
|
||||
|
||||
@@ -1397,7 +1397,7 @@ def get_material_request_items(
|
||||
"sales_order": sales_order,
|
||||
"description": row.get("description"),
|
||||
"uom": row.get("purchase_uom") or row.get("stock_uom"),
|
||||
"main_bom_item": row.get("main_bom_item"),
|
||||
"main_item_code": row.get("main_bom_item"),
|
||||
}
|
||||
|
||||
|
||||
@@ -1557,6 +1557,8 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
|
||||
"item_code": sa_row.production_item,
|
||||
"required_qty": sa_row.qty,
|
||||
"include_exploded_items": 0,
|
||||
"sales_order": sa_row.sales_order,
|
||||
"main_bom_item": sa_row.parent_item_code,
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -1660,6 +1662,7 @@ def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_d
|
||||
"stock_uom": item_master.stock_uom,
|
||||
"conversion_factor": conversion_factor,
|
||||
"safety_stock": item_master.safety_stock,
|
||||
"main_bom_item": data.get("main_bom_item"),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -1254,8 +1254,10 @@ class TestProductionPlan(FrappeTestCase):
|
||||
plan.get_sub_assembly_items()
|
||||
|
||||
mr_items = []
|
||||
expected_main_item_by_mr_item = {"ChildPart1 For MR": "SubAssembly1-1 For MR"}
|
||||
for row in plan.sub_assembly_items:
|
||||
mr_items.append(row.production_item)
|
||||
expected_main_item_by_mr_item[row.production_item] = row.parent_item_code
|
||||
row.type_of_manufacturing = "Material Request"
|
||||
|
||||
plan.save()
|
||||
@@ -1265,6 +1267,10 @@ class TestProductionPlan(FrappeTestCase):
|
||||
for item_code in mr_items:
|
||||
self.assertTrue(item_code in validate_mr_items)
|
||||
|
||||
main_item_by_mr_item = {item.get("item_code"): item.get("main_item_code") for item in items}
|
||||
for item_code, main_item_code in expected_main_item_by_mr_item.items():
|
||||
self.assertEqual(main_item_by_mr_item[item_code], main_item_code)
|
||||
|
||||
def test_resered_qty_for_production_plan_for_material_requests(self):
|
||||
from erpnext.stock.utils import get_or_make_bin
|
||||
|
||||
|
||||
@@ -12,21 +12,10 @@ frappe.ui.form.on("Work Order", {
|
||||
frm.ignore_doctypes_on_cancel_all = ["Serial and Batch Bundle"];
|
||||
|
||||
// Set query for warehouses
|
||||
frm.set_query("wip_warehouse", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("source_warehouse", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
frm.events.set_company_filters(frm, "wip_warehouse");
|
||||
frm.events.set_company_filters(frm, "source_warehouse");
|
||||
frm.events.set_company_filters(frm, "fg_warehouse");
|
||||
frm.events.set_company_filters(frm, "scrap_warehouse");
|
||||
|
||||
frm.set_query("source_warehouse", "required_items", function () {
|
||||
return {
|
||||
@@ -44,24 +33,6 @@ frappe.ui.form.on("Work Order", {
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("fg_warehouse", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("scrap_warehouse", function () {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
// Set query for BOM
|
||||
frm.set_query("bom_no", function () {
|
||||
if (frm.doc.production_item) {
|
||||
@@ -118,6 +89,16 @@ frappe.ui.form.on("Work Order", {
|
||||
});
|
||||
},
|
||||
|
||||
set_company_filters(frm, fieldname) {
|
||||
frm.set_query(fieldname, () => {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
onload: function (frm) {
|
||||
if (!frm.doc.status) frm.doc.status = "Draft";
|
||||
|
||||
@@ -315,7 +296,7 @@ frappe.ui.form.on("Work Order", {
|
||||
{
|
||||
fieldtype: "Data",
|
||||
fieldname: "name",
|
||||
label: __("Operation Id"),
|
||||
label: __("Operation ID"),
|
||||
},
|
||||
{
|
||||
fieldtype: "Float",
|
||||
@@ -385,6 +366,7 @@ frappe.ui.form.on("Work Order", {
|
||||
|
||||
if (pending_qty) {
|
||||
dialog.fields_dict.operations.df.data.push({
|
||||
__checked: 1,
|
||||
name: data.name,
|
||||
operation: data.operation,
|
||||
workstation: data.workstation,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_bulk_edit": 1,
|
||||
"allow_import": 1,
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2025-04-09 12:09:40.634472",
|
||||
@@ -249,6 +250,7 @@
|
||||
"fieldname": "wip_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Work-in-Progress Warehouse",
|
||||
"link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]",
|
||||
"mandatory_depends_on": "eval:!doc.skip_transfer || doc.from_wip_warehouse",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
@@ -257,6 +259,7 @@
|
||||
"fieldname": "fg_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Target Warehouse",
|
||||
"link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]",
|
||||
"options": "Warehouse",
|
||||
"reqd": 1
|
||||
},
|
||||
@@ -269,6 +272,7 @@
|
||||
"fieldname": "scrap_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Scrap Warehouse",
|
||||
"link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
@@ -498,6 +502,7 @@
|
||||
"fieldname": "source_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Source Warehouse",
|
||||
"link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
@@ -602,7 +607,7 @@
|
||||
"image_field": "image",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-06-21 00:55:45.916224",
|
||||
"modified": "2026-05-19 12:20:38.102403",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Work Order",
|
||||
|
||||
@@ -158,7 +158,7 @@ class WorkOrder(Document):
|
||||
self.calculate_operating_cost()
|
||||
self.validate_qty()
|
||||
self.validate_transfer_against()
|
||||
self.validate_operation_time()
|
||||
self.validate_operations()
|
||||
self.status = self.get_status()
|
||||
self.validate_workstation_type()
|
||||
self.reset_use_multi_level_bom()
|
||||
@@ -406,7 +406,7 @@ class WorkOrder(Document):
|
||||
elif self.docstatus == 1:
|
||||
if status not in ["Closed", "Stopped"]:
|
||||
status = "Not Started"
|
||||
if flt(self.material_transferred_for_manufacturing) > 0:
|
||||
if flt(self.material_transferred_for_manufacturing) > 0 or self.skip_transfer:
|
||||
status = "In Process"
|
||||
|
||||
precision = frappe.get_precision("Work Order", "produced_qty")
|
||||
@@ -1120,9 +1120,12 @@ class WorkOrder(Document):
|
||||
title=_("Missing value"),
|
||||
)
|
||||
|
||||
def validate_operation_time(self):
|
||||
def validate_operations(self):
|
||||
for d in self.operations:
|
||||
if not d.time_in_mins > 0:
|
||||
if not d.batch_size or d.batch_size <= 0:
|
||||
d.batch_size = 1
|
||||
|
||||
if d.time_in_mins <= 0:
|
||||
frappe.throw(_("Operation Time must be greater than 0 for Operation {0}").format(d.operation))
|
||||
|
||||
def update_required_items(self):
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_bulk_edit": 1,
|
||||
"creation": "2016-04-18 07:38:26.314642",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
@@ -46,6 +47,7 @@
|
||||
"ignore_user_permissions": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Source Warehouse",
|
||||
"link_filters": "[[\"Warehouse\",\"disabled\",\"=\",0],[\"Warehouse\",\"is_group\",\"=\",0]]",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
@@ -151,7 +153,7 @@
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-12-02 11:16:05.081613",
|
||||
"modified": "2026-05-12 12:05:16.687866",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Work Order Item",
|
||||
|
||||
@@ -185,10 +185,11 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "batch_size",
|
||||
"fieldtype": "Float",
|
||||
"label": "Batch Size",
|
||||
"read_only": 1
|
||||
"non_negative": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "sequence_id",
|
||||
@@ -225,14 +226,15 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-05-15 15:10:06.885440",
|
||||
"modified": "2026-05-27 12:56:37.240431",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Work Order Operation",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -434,3 +434,5 @@ erpnext.patches.v16_0.add_portal_redirects
|
||||
erpnext.patches.v16_0.update_order_qty_and_requested_qty_based_on_mr_and_po
|
||||
erpnext.patches.v16_0.depends_on_inv_dimensions
|
||||
erpnext.patches.v16_0.clear_procedures_from_receivable_report
|
||||
erpnext.patches.v16_0.migrate_address_contact_custom_fields
|
||||
erpnext.patches.v15_0.set_main_item_code_in_material_request_plan_item
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc("manufacturing", "doctype", "material_request_plan_item")
|
||||
|
||||
if not frappe.db.has_column("Material Request Plan Item", "main_item_code"):
|
||||
return
|
||||
|
||||
for row in get_material_request_plan_items():
|
||||
if row.main_item_code:
|
||||
continue
|
||||
|
||||
main_item_code = get_main_item_code(row)
|
||||
if main_item_code:
|
||||
frappe.db.set_value(
|
||||
"Material Request Plan Item",
|
||||
row.name,
|
||||
"main_item_code",
|
||||
main_item_code,
|
||||
update_modified=False,
|
||||
)
|
||||
|
||||
|
||||
def get_material_request_plan_items():
|
||||
return frappe.get_all(
|
||||
"Material Request Plan Item",
|
||||
fields=["name", "parent", "item_code", "sales_order", "main_item_code"],
|
||||
)
|
||||
|
||||
|
||||
def get_main_item_code(row):
|
||||
return (
|
||||
get_main_item_code_from_sub_assembly(row)
|
||||
or get_main_item_code_from_sub_assembly_bom(row)
|
||||
or get_main_item_code_from_production_plan_bom(row)
|
||||
)
|
||||
|
||||
|
||||
def get_main_item_code_from_sub_assembly(row):
|
||||
sub_assembly = frappe.db.get_value(
|
||||
"Production Plan Sub Assembly Item",
|
||||
get_filters(
|
||||
row,
|
||||
{
|
||||
"parent": row.parent,
|
||||
"production_item": row.item_code,
|
||||
},
|
||||
),
|
||||
"parent_item_code",
|
||||
)
|
||||
|
||||
return sub_assembly
|
||||
|
||||
|
||||
def get_main_item_code_from_sub_assembly_bom(row):
|
||||
for sub_assembly in get_sub_assembly_items(row):
|
||||
if item_exists_in_bom(row.item_code, sub_assembly.bom_no):
|
||||
return frappe.db.get_value("BOM", sub_assembly.bom_no, "item")
|
||||
|
||||
|
||||
def get_main_item_code_from_production_plan_bom(row):
|
||||
for production_plan_item in get_production_plan_items(row):
|
||||
if item_exists_in_bom(row.item_code, production_plan_item.bom_no):
|
||||
return frappe.db.get_value("BOM", production_plan_item.bom_no, "item")
|
||||
|
||||
|
||||
def get_sub_assembly_items(row):
|
||||
return frappe.get_all(
|
||||
"Production Plan Sub Assembly Item",
|
||||
filters=get_filters(row, {"parent": row.parent}),
|
||||
fields=["bom_no"],
|
||||
)
|
||||
|
||||
|
||||
def get_production_plan_items(row):
|
||||
return frappe.get_all(
|
||||
"Production Plan Item",
|
||||
filters=get_filters(row, {"parent": row.parent}),
|
||||
fields=["bom_no"],
|
||||
)
|
||||
|
||||
|
||||
def get_filters(row, filters):
|
||||
if row.sales_order:
|
||||
filters["sales_order"] = row.sales_order
|
||||
|
||||
return filters
|
||||
|
||||
|
||||
def item_exists_in_bom(item_code, bom_no):
|
||||
if not bom_no:
|
||||
return False
|
||||
|
||||
return frappe.db.exists("BOM Item", {"parent": bom_no, "item_code": item_code}) or frappe.db.exists(
|
||||
"BOM Explosion Item", {"parent": bom_no, "item_code": item_code}
|
||||
)
|
||||
@@ -0,0 +1,16 @@
|
||||
import frappe
|
||||
|
||||
from erpnext.setup.install import create_address_and_contact_custom_fields
|
||||
|
||||
|
||||
def execute():
|
||||
"""Replace fixture-based custom fields on Address and Contact with programmatic ones."""
|
||||
for custom_field in (
|
||||
"Address-tax_category",
|
||||
"Address-is_your_company_address",
|
||||
"Contact-is_billing_contact",
|
||||
):
|
||||
if frappe.db.exists("Custom Field", custom_field):
|
||||
frappe.delete_doc("Custom Field", custom_field, ignore_missing=True, force=True)
|
||||
|
||||
create_address_and_contact_custom_fields()
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"beta": 1,
|
||||
"creation": "2016-04-22 05:27:52.109319",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
@@ -87,7 +86,7 @@
|
||||
],
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2022-12-19 21:10:29.127277",
|
||||
"modified": "2026-05-30 20:51:04.415019",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Portal",
|
||||
"name": "Homepage",
|
||||
@@ -114,6 +113,7 @@
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
|
||||
@@ -176,6 +176,7 @@
|
||||
"fieldtype": "Link",
|
||||
"in_global_search": 1,
|
||||
"label": "Customer",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "customer",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "Customer",
|
||||
@@ -190,6 +191,7 @@
|
||||
"fieldname": "sales_order",
|
||||
"fieldtype": "Link",
|
||||
"label": "Sales Order",
|
||||
"no_copy": 1,
|
||||
"options": "Sales Order"
|
||||
},
|
||||
{
|
||||
@@ -462,7 +464,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"max_attachments": 4,
|
||||
"modified": "2025-08-21 17:57:58.314809",
|
||||
"modified": "2026-05-22 16:45:50.762759",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Projects",
|
||||
"name": "Project",
|
||||
|
||||
@@ -176,14 +176,9 @@ erpnext.buying = {
|
||||
callback: (r) => {
|
||||
if (!r.message) return;
|
||||
|
||||
if (!this.frm.doc.billing_address) {
|
||||
this.frm.set_value("billing_address", r.message.primary_address || "");
|
||||
}
|
||||
this.frm.set_value("billing_address", r.message.primary_address || "");
|
||||
|
||||
if (
|
||||
frappe.meta.has_field(this.frm.doc.doctype, "shipping_address") &&
|
||||
!this.frm.doc.shipping_address
|
||||
) {
|
||||
if (frappe.meta.has_field(this.frm.doc.doctype, "shipping_address")) {
|
||||
this.frm.set_value("shipping_address", r.message.shipping_address || "");
|
||||
}
|
||||
},
|
||||
|
||||
@@ -418,7 +418,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onload_post_render() {
|
||||
if(this.frm.doc.__islocal && !(this.frm.doc.taxes || []).length
|
||||
&& !this.frm.doc.__onload?.load_after_mapping) {
|
||||
@@ -659,15 +658,23 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
},
|
||||
async () => {
|
||||
// for internal customer instead of pricing rule directly apply valuation rate on item
|
||||
const fetch_valuation_rate_for_internal_transactions = await frappe.db.get_single_value(
|
||||
"Accounts Settings", "fetch_valuation_rate_for_internal_transaction"
|
||||
);
|
||||
if ((me.frm.doc.is_internal_customer || me.frm.doc.is_internal_supplier) && fetch_valuation_rate_for_internal_transactions) {
|
||||
me.get_incoming_rate(item, me.frm.posting_date, me.frm.posting_time,
|
||||
me.frm.doc.doctype, me.frm.doc.company);
|
||||
} else {
|
||||
me.frm.script_manager.trigger("price_list_rate", cdt, cdn);
|
||||
if ((me.frm.doc.is_internal_customer || me.frm.doc.is_internal_supplier)) {
|
||||
const fetch_valuation_rate_for_internal_transactions = await frappe.db.get_single_value(
|
||||
"Accounts Settings", "fetch_valuation_rate_for_internal_transaction"
|
||||
);
|
||||
if (fetch_valuation_rate_for_internal_transactions) {
|
||||
me.get_incoming_rate(
|
||||
item,
|
||||
me.frm.posting_date,
|
||||
me.frm.posting_time,
|
||||
me.frm.doc.doctype,
|
||||
me.frm.doc.company
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
me.frm.script_manager.trigger("price_list_rate", cdt, cdn);
|
||||
},
|
||||
() => {
|
||||
if (me.frm.doc.is_internal_customer || me.frm.doc.is_internal_supplier) {
|
||||
@@ -2506,11 +2513,29 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
method: "erpnext.controllers.stock_controller.check_item_quality_inspection",
|
||||
args: {
|
||||
doctype: this.frm.doc.doctype,
|
||||
items: this.frm.doc.items
|
||||
docstatus: this.frm.doc.docstatus,
|
||||
items: this.frm.doc.items,
|
||||
},
|
||||
freeze: true,
|
||||
callback: function (r) {
|
||||
r.message.forEach(item => {
|
||||
if (r.message.length == 0) {
|
||||
let type = inspection_type === "Incoming" ? "Purchase" : "Delivery";
|
||||
let fieldname =
|
||||
inspection_type === "Incoming"
|
||||
? "Inspection Required before Purchase"
|
||||
: "Inspection Required before Delivery";
|
||||
|
||||
frappe.msgprint({
|
||||
title: __("Quality Inspection Not Configured"),
|
||||
message: __(`Enable <b>{0}</b> on the Item master to proceed with {1} inspection.`, [
|
||||
fieldname,
|
||||
type,
|
||||
]),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
r.message.forEach((item) => {
|
||||
if (me.has_inspection_required(item)) {
|
||||
let dialog_items = dialog.fields_dict.items;
|
||||
dialog_items.df.data.push({
|
||||
|
||||
@@ -651,6 +651,7 @@ erpnext.utils.update_child_items = function (opts) {
|
||||
read_only: 0,
|
||||
disabled: 0,
|
||||
label: __("Item Code"),
|
||||
formatter: (value) => value,
|
||||
get_query: function () {
|
||||
let filters;
|
||||
if (frm.doc.doctype == "Sales Order") {
|
||||
|
||||
@@ -472,6 +472,8 @@ erpnext.SerialBatchPackageSelector = class SerialNoBatchBundleUpdate {
|
||||
warehouse:
|
||||
this.item.s_warehouse || this.item.t_warehouse || this.item.warehouse,
|
||||
is_inward: is_inward,
|
||||
posting_date: this.frm.doc.posting_date,
|
||||
posting_time: this.frm.doc.posting_time,
|
||||
include_expired_batches: include_expired_batches,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -305,6 +305,7 @@
|
||||
"fieldname": "customer_primary_contact",
|
||||
"fieldtype": "Link",
|
||||
"label": "Customer Primary Contact",
|
||||
"no_copy": 1,
|
||||
"options": "Contact"
|
||||
},
|
||||
{
|
||||
@@ -312,6 +313,7 @@
|
||||
"fieldname": "mobile_no",
|
||||
"fieldtype": "Read Only",
|
||||
"label": "Mobile No",
|
||||
"no_copy": 1,
|
||||
"options": "Mobile"
|
||||
},
|
||||
{
|
||||
@@ -319,6 +321,7 @@
|
||||
"fieldname": "email_id",
|
||||
"fieldtype": "Read Only",
|
||||
"label": "Email Id",
|
||||
"no_copy": 1,
|
||||
"options": "Email"
|
||||
},
|
||||
{
|
||||
@@ -330,12 +333,14 @@
|
||||
"fieldname": "customer_primary_address",
|
||||
"fieldtype": "Link",
|
||||
"label": "Customer Primary Address",
|
||||
"no_copy": 1,
|
||||
"options": "Address"
|
||||
},
|
||||
{
|
||||
"fieldname": "primary_address",
|
||||
"fieldtype": "Text",
|
||||
"label": "Primary Address",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -590,14 +595,16 @@
|
||||
"fieldname": "first_name",
|
||||
"fieldtype": "Read Only",
|
||||
"hidden": 1,
|
||||
"label": "First Name"
|
||||
"label": "First Name",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "customer_primary_contact.last_name",
|
||||
"fieldname": "last_name",
|
||||
"fieldtype": "Read Only",
|
||||
"hidden": 1,
|
||||
"label": "Last Name"
|
||||
"label": "Last Name",
|
||||
"no_copy": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-user",
|
||||
@@ -611,7 +618,7 @@
|
||||
"link_fieldname": "party"
|
||||
}
|
||||
],
|
||||
"modified": "2026-01-21 17:23:42.151114",
|
||||
"modified": "2026-05-29 16:52:59.441272",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Customer",
|
||||
|
||||
@@ -308,7 +308,6 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(doc.quotation_to=='Customer' && doc.party_name)",
|
||||
"fieldname": "col_break98",
|
||||
"fieldtype": "Column Break",
|
||||
"width": "50%"
|
||||
@@ -1108,7 +1107,7 @@
|
||||
"idx": 82,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-07-31 17:23:48.875382",
|
||||
"modified": "2026-05-30 17:40:02.667637",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Quotation",
|
||||
|
||||
@@ -26,7 +26,7 @@ frappe.ui.form.on("Sales Order", {
|
||||
let color;
|
||||
if (!doc.qty && frm.doc.has_unit_price_items) {
|
||||
color = "yellow";
|
||||
} else if (doc.stock_qty <= doc.actual_qty) {
|
||||
} else if (doc.stock_qty - doc.delivered_qty <= doc.actual_qty) {
|
||||
color = "green";
|
||||
} else {
|
||||
color = "orange";
|
||||
|
||||
@@ -10,6 +10,7 @@ import frappe.utils
|
||||
from frappe import _, qb
|
||||
from frappe.contacts.doctype.address.address import get_company_address
|
||||
from frappe.desk.notifications import clear_doctype_notifications
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.model.utils import get_fetch_values
|
||||
from frappe.query_builder.functions import Sum
|
||||
@@ -20,7 +21,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||
update_linked_doc,
|
||||
validate_inter_company_party,
|
||||
)
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.party import CROSS_PARTY_FIELD_NO_MAP, get_party_account
|
||||
from erpnext.controllers.selling_controller import SellingController
|
||||
from erpnext.manufacturing.doctype.blanket_order.blanket_order import (
|
||||
validate_against_blanket_order,
|
||||
@@ -385,7 +386,7 @@ class SalesOrder(SellingController):
|
||||
and not cint(d.delivered_by_supplier)
|
||||
):
|
||||
frappe.throw(
|
||||
_("Delivery warehouse required for stock item {0}").format(d.item_code), WarehouseRequired
|
||||
_("Source warehouse required for stock item {0}").format(d.item_code), WarehouseRequired
|
||||
)
|
||||
|
||||
def validate_with_previous_doc(self):
|
||||
@@ -1332,7 +1333,9 @@ def get_events(start, end, filters=None):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_purchase_order_for_default_supplier(source_name, selected_items=None, target_doc=None):
|
||||
def make_purchase_order_for_default_supplier(
|
||||
source_name: str, selected_items: str | list | None = None, target_doc: str | Document | None = None
|
||||
):
|
||||
"""Creates Purchase Order for each Supplier. Returns a list of doc objects."""
|
||||
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
@@ -1361,7 +1364,6 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
|
||||
target.shipping_rule = ""
|
||||
target.tc_name = ""
|
||||
target.terms = ""
|
||||
target.payment_terms_template = ""
|
||||
target.payment_schedule = []
|
||||
|
||||
default_price_list = frappe.get_value("Supplier", supplier, "default_price_list")
|
||||
@@ -1418,16 +1420,7 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
|
||||
{
|
||||
"Sales Order": {
|
||||
"doctype": "Purchase Order",
|
||||
"field_no_map": [
|
||||
"address_display",
|
||||
"contact_display",
|
||||
"contact_mobile",
|
||||
"contact_email",
|
||||
"contact_person",
|
||||
"taxes_and_charges",
|
||||
"shipping_address",
|
||||
"dispatch_address",
|
||||
],
|
||||
"field_no_map": [*CROSS_PARTY_FIELD_NO_MAP],
|
||||
"validation": {"docstatus": ["=", 1]},
|
||||
},
|
||||
"Sales Order Item": {
|
||||
@@ -1492,7 +1485,9 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_purchase_order(source_name, selected_items=None, target_doc=None):
|
||||
def make_purchase_order(
|
||||
source_name: str, selected_items: str | list | None = None, target_doc: str | Document | None = None
|
||||
):
|
||||
if not selected_items:
|
||||
return
|
||||
|
||||
@@ -1520,7 +1515,6 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
|
||||
target.shipping_rule = ""
|
||||
target.tc_name = ""
|
||||
target.terms = ""
|
||||
target.payment_terms_template = ""
|
||||
target.payment_schedule = []
|
||||
|
||||
if is_drop_ship_order(target):
|
||||
@@ -1559,16 +1553,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
|
||||
{
|
||||
"Sales Order": {
|
||||
"doctype": "Purchase Order",
|
||||
"field_no_map": [
|
||||
"address_display",
|
||||
"contact_display",
|
||||
"contact_mobile",
|
||||
"contact_email",
|
||||
"contact_person",
|
||||
"taxes_and_charges",
|
||||
"shipping_address",
|
||||
"dispatch_address",
|
||||
],
|
||||
"field_no_map": [*CROSS_PARTY_FIELD_NO_MAP],
|
||||
"validation": {"docstatus": ["=", 1]},
|
||||
},
|
||||
"Sales Order Item": {
|
||||
|
||||
@@ -24,6 +24,8 @@ from erpnext.selling.doctype.sales_order.sales_order import (
|
||||
create_pick_list,
|
||||
make_delivery_note,
|
||||
make_material_request,
|
||||
make_purchase_order,
|
||||
make_purchase_order_for_default_supplier,
|
||||
make_raw_material_request,
|
||||
make_sales_invoice,
|
||||
make_work_orders,
|
||||
@@ -692,6 +694,40 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
||||
workflow.is_active = 0
|
||||
workflow.save()
|
||||
|
||||
def test_update_child_qty_rate_follows_allow_edit(self):
|
||||
from frappe.model.workflow import apply_workflow
|
||||
|
||||
workflow = make_sales_order_edit_perm_workflow()
|
||||
so = make_sales_order(item_code="_Test Item", qty=1, rate=150, do_not_submit=1)
|
||||
apply_workflow(so, "Approve")
|
||||
|
||||
trans_item = json.dumps(
|
||||
[{"item_code": "_Test Item", "rate": 150, "qty": 2, "docname": so.items[0].name}]
|
||||
)
|
||||
|
||||
mover = "test@example.com"
|
||||
mover_user = frappe.get_doc("User", mover)
|
||||
mover_user.add_roles("Sales User", "Test Junior Approver")
|
||||
with self.set_user(mover):
|
||||
# transitioned the doc into Approved but is not the configured editor
|
||||
self.assertRaises(
|
||||
frappe.ValidationError, update_child_qty_rate, "Sales Order", trans_item, so.name
|
||||
)
|
||||
|
||||
editor = "test2@example.com"
|
||||
editor_user = frappe.get_doc("User", editor)
|
||||
editor_user.add_roles("Sales User", "Test Approver")
|
||||
with self.set_user(editor):
|
||||
# Test Approver is the "Only Allow Edit For" role on Approved
|
||||
update_child_qty_rate("Sales Order", trans_item, so.name)
|
||||
so.reload()
|
||||
self.assertEqual(so.items[0].qty, 2)
|
||||
|
||||
mover_user.remove_roles("Sales User", "Test Junior Approver", "Test Approver")
|
||||
editor_user.remove_roles("Sales User", "Test Junior Approver", "Test Approver")
|
||||
workflow.is_active = 0
|
||||
workflow.save()
|
||||
|
||||
def test_material_request_for_product_bundle(self):
|
||||
# Create the Material Request from the sales order for the Packing Items
|
||||
# Check whether the material request has the correct packing item or not.
|
||||
@@ -1330,8 +1366,6 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
||||
Tests if the the Product Bundles in the Items table of Sales Orders are replaced with
|
||||
their child items(from the Packed Items table) on creating a Purchase Order from it.
|
||||
"""
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order
|
||||
|
||||
product_bundle = make_item("_Test Product Bundle", {"is_stock_item": 0})
|
||||
make_item("_Test Bundle Item 1", {"is_stock_item": 1})
|
||||
make_item("_Test Bundle Item 2", {"is_stock_item": 1})
|
||||
@@ -1360,8 +1394,6 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
||||
"""
|
||||
Tests if the packed item's `ordered_qty` is updated with the quantity of the Purchase Order
|
||||
"""
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order
|
||||
|
||||
product_bundle = make_item("_Test Product Bundle", {"is_stock_item": 0})
|
||||
make_item("_Test Bundle Item 1", {"is_stock_item": 1})
|
||||
make_item("_Test Bundle Item 2", {"is_stock_item": 1})
|
||||
@@ -2385,8 +2417,6 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
||||
self.assertRaises(frappe.ValidationError, so1.update_status, "Draft")
|
||||
|
||||
def test_item_tax_transfer_from_sales_to_purchase(self):
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order
|
||||
|
||||
item_tax = frappe.new_doc("Item Tax Template")
|
||||
item_tax.title = "Test Item Tax Template"
|
||||
item_tax.company = "_Test Company"
|
||||
@@ -2487,6 +2517,33 @@ class TestSalesOrder(AccountsTestMixin, FrappeTestCase):
|
||||
self.assertFalse(so.per_billed)
|
||||
self.assertEqual(so.status, "To Deliver and Bill")
|
||||
|
||||
def test_make_purchase_order_does_not_inherit_party_fields(self):
|
||||
"""
|
||||
Customer-derived fields must not leak from a drop-ship SO into the PO.
|
||||
"""
|
||||
so_items = [
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
"warehouse": "",
|
||||
"qty": 1,
|
||||
"rate": 100,
|
||||
"delivered_by_supplier": 1,
|
||||
"supplier": "_Test Supplier",
|
||||
}
|
||||
]
|
||||
so = make_sales_order(item_list=so_items, do_not_submit=True)
|
||||
so.tax_category = "_Test Tax Category 1"
|
||||
so.language = "ar"
|
||||
so.payment_terms_template = "_Test Payment Term Template"
|
||||
so.submit()
|
||||
|
||||
po = make_purchase_order_for_default_supplier(so.name, selected_items=so_items)[0]
|
||||
|
||||
supplier = frappe.get_doc("Supplier", "_Test Supplier")
|
||||
self.assertEqual(po.tax_category or None, supplier.tax_category or None)
|
||||
self.assertEqual(po.language or None, supplier.language or None)
|
||||
self.assertEqual(po.payment_terms_template or None, supplier.payment_terms or None)
|
||||
|
||||
def test_pending_quantity_after_update_item_during_invoice_creation(self):
|
||||
so = make_sales_order(qty=30, rate=100)
|
||||
|
||||
@@ -2683,3 +2740,41 @@ def make_sales_order_workflow():
|
||||
workflow.insert(ignore_permissions=True)
|
||||
|
||||
return workflow
|
||||
|
||||
|
||||
def make_sales_order_edit_perm_workflow():
|
||||
if frappe.db.exists("Workflow", "SO Edit Perm Workflow"):
|
||||
doc = frappe.get_doc("Workflow", "SO Edit Perm Workflow")
|
||||
doc.set("is_active", 1)
|
||||
doc.save()
|
||||
return doc
|
||||
|
||||
frappe.get_doc(doctype="Role", role_name="Test Junior Approver").insert(ignore_if_duplicate=True)
|
||||
frappe.get_doc(doctype="Role", role_name="Test Approver").insert(ignore_if_duplicate=True)
|
||||
frappe.cache().hdel("roles", frappe.session.user)
|
||||
|
||||
workflow = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Workflow",
|
||||
"workflow_name": "SO Edit Perm Workflow",
|
||||
"document_type": "Sales Order",
|
||||
"workflow_state_field": "workflow_state",
|
||||
"is_active": 1,
|
||||
"send_email_alert": 0,
|
||||
}
|
||||
)
|
||||
workflow.append("states", dict(state="Pending", allow_edit="All"))
|
||||
workflow.append("states", dict(state="Approved", allow_edit="Test Approver", doc_status=1))
|
||||
workflow.append(
|
||||
"transitions",
|
||||
dict(
|
||||
state="Pending",
|
||||
action="Approve",
|
||||
next_state="Approved",
|
||||
allowed="Test Junior Approver",
|
||||
allow_self_approval=1,
|
||||
),
|
||||
)
|
||||
workflow.insert(ignore_permissions=True)
|
||||
|
||||
return workflow
|
||||
|
||||
@@ -377,42 +377,80 @@ def get_past_order_list(search_term, status, limit=20):
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_customer_info(fieldname, customer, value=""):
|
||||
customer_doc = frappe.get_doc("Customer", customer)
|
||||
customer_doc.check_permission("write")
|
||||
|
||||
if fieldname == "loyalty_program":
|
||||
frappe.db.set_value("Customer", customer, "loyalty_program", value)
|
||||
customer_doc.loyalty_program = value
|
||||
else:
|
||||
contact = customer_doc.get("customer_primary_contact")
|
||||
if not contact:
|
||||
Contact = DocType("Contact")
|
||||
DynamicLink = DocType("Dynamic Link")
|
||||
|
||||
contact = frappe.get_cached_value("Customer", customer, "customer_primary_contact")
|
||||
if not contact:
|
||||
contact = frappe.db.sql(
|
||||
"""
|
||||
SELECT parent FROM `tabDynamic Link`
|
||||
WHERE
|
||||
parenttype = 'Contact' AND
|
||||
parentfield = 'links' AND
|
||||
link_doctype = 'Customer' AND
|
||||
link_name = %s
|
||||
""",
|
||||
(customer),
|
||||
as_dict=1,
|
||||
)
|
||||
contact = contact[0].get("parent") if contact else None
|
||||
# Inner join with Contact DocType, to priorities records that have is_primary_contact set.
|
||||
query = (
|
||||
frappe.qb.from_(DynamicLink)
|
||||
.join(Contact)
|
||||
.on(DynamicLink.parent == Contact.name)
|
||||
.select(DynamicLink.parent)
|
||||
.where(
|
||||
(DynamicLink.link_name == customer)
|
||||
& (DynamicLink.parentfield == "links")
|
||||
& (DynamicLink.parenttype == "Contact")
|
||||
& (DynamicLink.link_doctype == "Customer")
|
||||
)
|
||||
.orderby(Contact.is_primary_contact, order=Order.desc)
|
||||
)
|
||||
|
||||
if not contact:
|
||||
new_contact = frappe.new_doc("Contact")
|
||||
new_contact.is_primary_contact = 1
|
||||
new_contact.first_name = customer
|
||||
new_contact.set("links", [{"link_doctype": "Customer", "link_name": customer}])
|
||||
new_contact.save()
|
||||
contact = new_contact.name
|
||||
frappe.db.set_value("Customer", customer, "customer_primary_contact", contact)
|
||||
contacts = query.run(pluck=DynamicLink.parent)
|
||||
|
||||
contact_doc = frappe.get_doc("Contact", contact)
|
||||
if fieldname == "email_id":
|
||||
contact_doc.set("email_ids", [{"email_id": value, "is_primary": 1}])
|
||||
frappe.db.set_value("Customer", customer, "email_id", value)
|
||||
elif fieldname == "mobile_no":
|
||||
contact_doc.set("phone_nos", [{"phone": value, "is_primary_mobile_no": 1}])
|
||||
frappe.db.set_value("Customer", customer, "mobile_no", value)
|
||||
contact_doc.save()
|
||||
contact = contacts[0] if contacts else None
|
||||
|
||||
if not contact:
|
||||
new_contact = frappe.new_doc("Contact")
|
||||
new_contact.is_primary_contact = 1
|
||||
new_contact.first_name = customer
|
||||
new_contact.set("links", [{"link_doctype": "Customer", "link_name": customer}])
|
||||
new_contact.save()
|
||||
contact = new_contact.name
|
||||
|
||||
def set_primary_phone_no_email(field, value):
|
||||
# Create new record instead deleting existing email or phone_no and setting the new row as primary.
|
||||
field_mapper = {
|
||||
"email_ids": {"field": "email_id", "primary": "is_primary"},
|
||||
"phone_nos": {"field": "phone", "primary": "is_primary_mobile_no"},
|
||||
}
|
||||
|
||||
value_already_exists = False
|
||||
for d in contact_doc.get(field):
|
||||
if d.get(field_mapper[field].get("field")) == value and not value_already_exists:
|
||||
d.set(field_mapper[field]["primary"], 1)
|
||||
value_already_exists = True
|
||||
continue
|
||||
d.set(field_mapper[field]["primary"], 0)
|
||||
|
||||
if not value_already_exists:
|
||||
contact_doc.append(
|
||||
field, {field_mapper[field]["field"]: value, field_mapper[field]["primary"]: 1}
|
||||
)
|
||||
|
||||
contact_doc = frappe.get_doc("Contact", contact)
|
||||
# setting is_primary_contact = 1 on Contact to refetch the same contact incase it's removed from Customer records.
|
||||
contact_doc.set("is_primary_contact", 1)
|
||||
if fieldname == "email_id":
|
||||
set_primary_phone_no_email("email_ids", value)
|
||||
elif fieldname == "mobile_no":
|
||||
set_primary_phone_no_email("phone_nos", value)
|
||||
# Saving contact_doc to set mobile_no and email.
|
||||
contact_doc.save()
|
||||
|
||||
# Auto-fetches from Contact DocType, no need to set values separately.
|
||||
customer_doc.customer_primary_contact = contact
|
||||
|
||||
# using save method instead db.set_value which bypasses the validation for loyalty program
|
||||
# and auto sets the mobile_no and email field on customer records.
|
||||
customer_doc.save()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -174,8 +174,8 @@ erpnext.PointOfSale.Controller = class {
|
||||
set_opening_entry_status() {
|
||||
this.page.set_title_sub(
|
||||
`<span class="indicator orange">
|
||||
<a class="text-muted" href="#Form/POS%20Opening%20Entry/${this.pos_opening}">
|
||||
Opened at ${frappe.datetime.str_to_user(this.pos_opening_time)}
|
||||
<a class="text-muted" href="#Form/POS%20Opening%20Entry/${encodeURIComponent(this.pos_opening)}">
|
||||
Opened at ${frappe.utils.escape_html(frappe.datetime.str_to_user(this.pos_opening_time))}
|
||||
</a>
|
||||
</span>`
|
||||
);
|
||||
|
||||
@@ -178,7 +178,7 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
me.$totals_section.find(".edit-cart-btn").click();
|
||||
}
|
||||
|
||||
const item_row_name = unescape($cart_item.attr("data-row-name"));
|
||||
const item_row_name = $cart_item.attr("data-row-name");
|
||||
me.events.cart_item_clicked({ name: item_row_name });
|
||||
this.numpad_value = "";
|
||||
});
|
||||
@@ -453,10 +453,10 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
<div class="customer-display">
|
||||
${this.get_customer_image()}
|
||||
<div class="customer-name-desc">
|
||||
<div class="customer-name">${customer_name}</div>
|
||||
<div class="customer-name">${frappe.utils.escape_html(customer_name)}</div>
|
||||
${get_customer_description()}
|
||||
</div>
|
||||
<div class="reset-customer-btn" data-customer="${escape(customer)}">
|
||||
<div class="reset-customer-btn" data-customer="${frappe.utils.escape_html(customer)}">
|
||||
<svg width="32" height="32" viewBox="0 0 14 14" fill="none">
|
||||
<path d="M4.93764 4.93759L7.00003 6.99998M9.06243 9.06238L7.00003 6.99998M7.00003 6.99998L4.93764 9.06238L9.06243 4.93759" stroke="#8D99A6"/>
|
||||
</svg>
|
||||
@@ -473,11 +473,13 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
if (!email_id && !mobile_no) {
|
||||
return `<div class="customer-desc">${__("Click to add email / phone")}</div>`;
|
||||
} else if (email_id && !mobile_no) {
|
||||
return `<div class="customer-desc">${email_id}</div>`;
|
||||
return `<div class="customer-desc">${frappe.utils.escape_html(email_id)}</div>`;
|
||||
} else if (mobile_no && !email_id) {
|
||||
return `<div class="customer-desc">${mobile_no}</div>`;
|
||||
return `<div class="customer-desc">${frappe.utils.escape_html(mobile_no)}</div>`;
|
||||
} else {
|
||||
return `<div class="customer-desc">${email_id} - ${mobile_no}</div>`;
|
||||
return `<div class="customer-desc">${frappe.utils.escape_html(
|
||||
email_id
|
||||
)} - ${frappe.utils.escape_html(mobile_no)}</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -485,9 +487,13 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
get_customer_image() {
|
||||
const { customer, image } = this.customer_info || {};
|
||||
if (image) {
|
||||
return `<div class="customer-image"><img src="${image}" alt="${image}""></div>`;
|
||||
return `<div class="customer-image"><img src="${frappe.utils.escape_html(
|
||||
image
|
||||
)}" alt="${frappe.utils.escape_html(image)}"></div>`;
|
||||
} else {
|
||||
return `<div class="customer-image customer-abbr">${frappe.get_abbr(customer)}</div>`;
|
||||
return `<div class="customer-image customer-abbr">${frappe.utils.escape_html(
|
||||
frappe.get_abbr(customer)
|
||||
)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -549,10 +555,10 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
if (t.tax_amount_after_discount_amount == 0.0) return;
|
||||
// if tax rate is 0, don't print it.
|
||||
const description = /[0-9]+/.test(t.description)
|
||||
? t.description
|
||||
? frappe.utils.escape_html(t.description)
|
||||
: t.rate != 0
|
||||
? `${t.description} @ ${t.rate}%`
|
||||
: t.description;
|
||||
? `${frappe.utils.escape_html(t.description)} @ ${t.rate}%`
|
||||
: frappe.utils.escape_html(t.description);
|
||||
return `<div class="tax-row">
|
||||
<div class="tax-label">${description}</div>
|
||||
<div class="tax-value">${format_currency(t.tax_amount_after_discount_amount, currency)}</div>
|
||||
@@ -566,8 +572,9 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
}
|
||||
|
||||
get_cart_item({ name }) {
|
||||
const item_selector = `.cart-item-wrapper[data-row-name="${escape(name)}"]`;
|
||||
return this.$cart_items_wrapper.find(item_selector);
|
||||
return this.$cart_items_wrapper.find(".cart-item-wrapper").filter(function () {
|
||||
return $(this).attr("data-row-name") === name;
|
||||
});
|
||||
}
|
||||
|
||||
get_item_from_frm(item) {
|
||||
@@ -597,7 +604,9 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
|
||||
if (!$item_to_update.length) {
|
||||
this.$cart_items_wrapper.append(
|
||||
`<div class="cart-item-wrapper" data-row-name="${escape(item_data.name)}"></div>
|
||||
`<div class="cart-item-wrapper" data-row-name="${frappe.utils.escape_html(
|
||||
item_data.name
|
||||
)}"></div>
|
||||
<div class="seperator"></div>`
|
||||
);
|
||||
$item_to_update = this.get_cart_item(item_data);
|
||||
@@ -607,7 +616,7 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
`${get_item_image_html()}
|
||||
<div class="item-name-desc">
|
||||
<div class="item-name">
|
||||
${item_data.item_name}
|
||||
${frappe.utils.escape_html(item_data.item_name)}
|
||||
</div>
|
||||
${get_description_html()}
|
||||
</div>
|
||||
@@ -636,7 +645,7 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
if (item_data.rate && item_data.amount && item_data.rate !== item_data.amount) {
|
||||
return `
|
||||
<div class="item-qty-rate">
|
||||
<div class="item-qty"><span>${item_data.qty || 0} ${item_data.uom}</span></div>
|
||||
<div class="item-qty"><span>${item_data.qty || 0} ${frappe.utils.escape_html(item_data.uom)}</span></div>
|
||||
<div class="item-rate-amount">
|
||||
<div class="item-rate">${format_currency(item_data.amount, currency)}</div>
|
||||
<div class="item-amount">${format_currency(item_data.rate, currency)}</div>
|
||||
@@ -645,7 +654,7 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
} else {
|
||||
return `
|
||||
<div class="item-qty-rate">
|
||||
<div class="item-qty"><span>${item_data.qty || 0} ${item_data.uom}</span></div>
|
||||
<div class="item-qty"><span>${item_data.qty || 0} ${frappe.utils.escape_html(item_data.uom)}</span></div>
|
||||
<div class="item-rate-amount">
|
||||
<div class="item-rate">${format_currency(item_data.rate, currency)}</div>
|
||||
</div>
|
||||
@@ -666,7 +675,7 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
}
|
||||
}
|
||||
item_data.description = frappe.ellipsis(item_data.description, 45);
|
||||
return `<div class="item-desc">${item_data.description}</div>`;
|
||||
return `<div class="item-desc">${frappe.utils.escape_html(item_data.description)}</div>`;
|
||||
}
|
||||
return ``;
|
||||
}
|
||||
@@ -678,22 +687,26 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
<div class="item-image">
|
||||
<img
|
||||
onerror="cur_pos.cart.handle_broken_image(this)"
|
||||
src="${image}" alt="${frappe.get_abbr(item_name)}"">
|
||||
src="${frappe.utils.escape_html(image)}" alt="${frappe.utils.escape_html(frappe.get_abbr(item_name))}">
|
||||
</div>`;
|
||||
} else {
|
||||
return `<div class="item-image item-abbr">${frappe.get_abbr(item_name)}</div>`;
|
||||
return `<div class="item-image item-abbr">${frappe.utils.escape_html(
|
||||
frappe.get_abbr(item_name)
|
||||
)}</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handle_broken_image($img) {
|
||||
const item_abbr = $($img).attr("alt");
|
||||
$($img).parent().replaceWith(`<div class="item-image item-abbr">${item_abbr}</div>`);
|
||||
$($img)
|
||||
.parent()
|
||||
.replaceWith(`<div class="item-image item-abbr">${frappe.utils.escape_html(item_abbr)}</div>`);
|
||||
}
|
||||
|
||||
update_selector_value_in_cart_item(selector, value, item) {
|
||||
const $item_to_update = this.get_cart_item(item);
|
||||
$item_to_update.attr(`data-${selector}`, escape(value));
|
||||
$item_to_update.attr(`data-${selector}`, value);
|
||||
}
|
||||
|
||||
toggle_checkout_btn(show_checkout) {
|
||||
@@ -892,8 +905,8 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
<div class="customer-display">
|
||||
${this.get_customer_image()}
|
||||
<div class="customer-name-desc">
|
||||
<div class="customer-name">${customer_name}</div>
|
||||
<div class="customer-desc">${customer}</div>
|
||||
<div class="customer-name">${frappe.utils.escape_html(customer_name)}</div>
|
||||
<div class="customer-desc">${frappe.utils.escape_html(customer)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="customer-fields-container">
|
||||
@@ -980,6 +993,7 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
customer: current_customer,
|
||||
value: this.value,
|
||||
},
|
||||
freeze: true,
|
||||
callback: (r) => {
|
||||
if (!r.exc) {
|
||||
me.customer_info[this.df.fieldname] = this.value;
|
||||
@@ -1029,9 +1043,11 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
};
|
||||
|
||||
transaction_container.append(
|
||||
`<div class="invoice-wrapper" data-invoice-name="${escape(invoice.name)}">
|
||||
`<div class="invoice-wrapper" data-invoice-name="${frappe.utils.escape_html(
|
||||
invoice.name
|
||||
)}">
|
||||
<div class="invoice-name-date">
|
||||
<div class="invoice-name">${invoice.name}</div>
|
||||
<div class="invoice-name">${frappe.utils.escape_html(invoice.name)}</div>
|
||||
<div class="invoice-date">${posting_datetime}</div>
|
||||
</div>
|
||||
<div class="invoice-total-status">
|
||||
@@ -1039,7 +1055,7 @@ erpnext.PointOfSale.ItemCart = class {
|
||||
${format_currency(invoice.grand_total, invoice.currency, frappe.sys_defaults.currency_precision) || 0}
|
||||
</div>
|
||||
<div class="invoice-status">
|
||||
<span class="indicator-pill whitespace-nowrap ${indicator_color[invoice.status]}">
|
||||
<span class="indicator-pill whitespace-nowrap ${indicator_color[invoice.status] || ""}">
|
||||
<span>${__(invoice.status)}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -128,25 +128,27 @@ erpnext.PointOfSale.ItemDetails = class {
|
||||
return ``;
|
||||
}
|
||||
|
||||
this.$item_name.html(item_name);
|
||||
this.$item_name.html(frappe.utils.escape_html(item_name));
|
||||
this.$item_description.html(get_description_html());
|
||||
this.$item_price.html(format_currency(price_list_rate, this.currency));
|
||||
if (!this.hide_images && image) {
|
||||
this.$item_image.html(
|
||||
`<img
|
||||
onerror="cur_pos.item_details.handle_broken_image(this)"
|
||||
class="h-full" src="${image}"
|
||||
alt="${frappe.get_abbr(item_name)}"
|
||||
class="h-full" src="${frappe.utils.escape_html(image)}"
|
||||
alt="${frappe.utils.escape_html(frappe.get_abbr(item_name))}"
|
||||
style="object-fit: cover;">`
|
||||
);
|
||||
} else {
|
||||
this.$item_image.html(`<div class="item-abbr">${frappe.get_abbr(item_name)}</div>`);
|
||||
this.$item_image.html(
|
||||
`<div class="item-abbr">${frappe.utils.escape_html(frappe.get_abbr(item_name))}</div>`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
handle_broken_image($img) {
|
||||
const item_abbr = $($img).attr("alt");
|
||||
$($img).replaceWith(`<div class="item-abbr">${item_abbr}</div>`);
|
||||
$($img).replaceWith(`<div class="item-abbr">${frappe.utils.escape_html(item_abbr)}</div>`);
|
||||
}
|
||||
|
||||
render_discount_dom(item) {
|
||||
|
||||
@@ -107,39 +107,45 @@ erpnext.PointOfSale.ItemSelector = class {
|
||||
<div class="flex items-center justify-center border-b-grey text-6xl text-grey-100" style="height:8rem; min-height:8rem">
|
||||
<img
|
||||
onerror="cur_pos.item_selector.handle_broken_image(this)"
|
||||
class="h-full item-img" src="${item_image}"
|
||||
alt="${frappe.get_abbr(item.item_name)}"
|
||||
class="h-full item-img" src="${frappe.utils.escape_html(item_image)}"
|
||||
alt="${frappe.utils.escape_html(frappe.get_abbr(item.item_name))}"
|
||||
>
|
||||
</div>`;
|
||||
} else {
|
||||
return `<div class="item-qty-pill">
|
||||
<span class="indicator-pill whitespace-nowrap ${indicator_color}">${qty_to_display}</span>
|
||||
</div>
|
||||
<div class="item-display abbr">${frappe.get_abbr(item.item_name)}</div>`;
|
||||
<div class="item-display abbr">${frappe.utils.escape_html(frappe.get_abbr(item.item_name))}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
return `<div class="item-wrapper"
|
||||
data-item-code="${escape(item.item_code)}" data-serial-no="${escape(serial_no)}"
|
||||
data-batch-no="${escape(batch_no)}" data-uom="${escape(uom)}"
|
||||
data-rate="${escape(price_list_rate || 0)}"
|
||||
data-stock-uom="${escape(item.stock_uom)}"
|
||||
title="${item.item_name}">
|
||||
data-item-code="${frappe.utils.escape_html(item.item_code)}" data-serial-no="${frappe.utils.escape_html(
|
||||
serial_no
|
||||
)}"
|
||||
data-batch-no="${frappe.utils.escape_html(batch_no)}" data-uom="${frappe.utils.escape_html(uom)}"
|
||||
data-rate="${frappe.utils.escape_html(price_list_rate || 0)}"
|
||||
data-stock-uom="${frappe.utils.escape_html(item.stock_uom)}"
|
||||
title="${frappe.utils.escape_html(item.item_name)}">
|
||||
|
||||
${get_item_image_html()}
|
||||
|
||||
<div class="item-detail">
|
||||
<div class="item-name">
|
||||
${frappe.ellipsis(item.item_name, 18)}
|
||||
${frappe.utils.escape_html(frappe.ellipsis(item.item_name, 18))}
|
||||
</div>
|
||||
<div class="item-rate">${format_currency(price_list_rate, item.currency, precision) || 0} / ${uom}</div>
|
||||
<div class="item-rate">${
|
||||
format_currency(price_list_rate, item.currency, precision) || 0
|
||||
} / ${frappe.utils.escape_html(uom)}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
handle_broken_image($img) {
|
||||
const item_abbr = $($img).attr("alt");
|
||||
$($img).parent().replaceWith(`<div class="item-display abbr">${item_abbr}</div>`);
|
||||
$($img)
|
||||
.parent()
|
||||
.replaceWith(`<div class="item-display abbr">${frappe.utils.escape_html(item_abbr)}</div>`);
|
||||
}
|
||||
|
||||
make_search_bar() {
|
||||
@@ -252,14 +258,13 @@ erpnext.PointOfSale.ItemSelector = class {
|
||||
|
||||
this.$component.on("click", ".item-wrapper", function () {
|
||||
const $item = $(this);
|
||||
const item_code = unescape($item.attr("data-item-code"));
|
||||
let batch_no = unescape($item.attr("data-batch-no"));
|
||||
let serial_no = unescape($item.attr("data-serial-no"));
|
||||
let uom = unescape($item.attr("data-uom"));
|
||||
let rate = unescape($item.attr("data-rate"));
|
||||
let stock_uom = unescape($item.attr("data-stock-uom"));
|
||||
const item_code = $item.attr("data-item-code");
|
||||
let batch_no = $item.attr("data-batch-no");
|
||||
let serial_no = $item.attr("data-serial-no");
|
||||
let uom = $item.attr("data-uom");
|
||||
let rate = $item.attr("data-rate");
|
||||
let stock_uom = $item.attr("data-stock-uom");
|
||||
|
||||
// escape(undefined) returns "undefined" then unescape returns "undefined"
|
||||
batch_no = batch_no === "undefined" ? undefined : batch_no;
|
||||
serial_no = serial_no === "undefined" ? undefined : serial_no;
|
||||
uom = uom === "undefined" ? undefined : uom;
|
||||
|
||||
@@ -38,7 +38,7 @@ erpnext.PointOfSale.PastOrderList = class {
|
||||
});
|
||||
const me = this;
|
||||
this.$invoices_container.on("click", ".invoice-wrapper", function () {
|
||||
const invoice_name = unescape($(this).attr("data-invoice-name"));
|
||||
const invoice_name = $(this).attr("data-invoice-name");
|
||||
|
||||
me.events.open_invoice_data(invoice_name);
|
||||
});
|
||||
@@ -99,14 +99,14 @@ erpnext.PointOfSale.PastOrderList = class {
|
||||
const posting_datetime = frappe.datetime.str_to_user(
|
||||
invoice.posting_date + " " + invoice.posting_time
|
||||
);
|
||||
return `<div class="invoice-wrapper" data-invoice-name="${escape(invoice.name)}">
|
||||
return `<div class="invoice-wrapper" data-invoice-name="${frappe.utils.escape_html(invoice.name)}">
|
||||
<div class="invoice-name-date">
|
||||
<div class="invoice-name">${invoice.name}</div>
|
||||
<div class="invoice-name">${frappe.utils.escape_html(invoice.name)}</div>
|
||||
<div class="invoice-date">
|
||||
<svg class="mr-2" width="12" height="12" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>
|
||||
</svg>
|
||||
${frappe.ellipsis(invoice.customer_name, 20)}
|
||||
${frappe.utils.escape_html(frappe.ellipsis(invoice.customer_name, 20))}
|
||||
</div>
|
||||
</div>
|
||||
<div class="invoice-total-status">
|
||||
|
||||
@@ -81,23 +81,27 @@ erpnext.PointOfSale.PastOrderSummary = class {
|
||||
|
||||
return `<div class="left-section">
|
||||
<div class="customer-section">
|
||||
<div class="customer-name">${doc.customer_name}</div>
|
||||
${is_customer_naming_by_customer_name ? `<div class="customer-code">${doc.customer}</div>` : ""}
|
||||
<div class="customer-email">${this.customer_email}</div>
|
||||
<div class="customer-name">${frappe.utils.escape_html(doc.customer_name)}</div>
|
||||
${
|
||||
is_customer_naming_by_customer_name
|
||||
? `<div class="customer-code">${frappe.utils.escape_html(doc.customer)}</div>`
|
||||
: ""
|
||||
}
|
||||
<div class="customer-email">${frappe.utils.escape_html(this.customer_email)}</div>
|
||||
</div>
|
||||
<div class="cashier">${__("Sold by")}: ${doc.owner}</div>
|
||||
<div class="cashier">${__("Sold by")}: ${frappe.utils.escape_html(doc.owner)}</div>
|
||||
</div>
|
||||
<div class="right-section">
|
||||
<div class="paid-amount">${format_currency(doc.paid_amount, doc.currency)}</div>
|
||||
<div class="invoice-name">${doc.name}</div>
|
||||
<div class="invoice-name">${frappe.utils.escape_html(doc.name)}</div>
|
||||
<span class="indicator-pill whitespace-nowrap ${indicator_color}"><span>${__(doc.status)}</span></span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
get_item_html(doc, item_data) {
|
||||
return `<div class="item-row-wrapper">
|
||||
<div class="item-name">${item_data.item_name}</div>
|
||||
<div class="item-qty">${item_data.qty || 0} ${item_data.uom}</div>
|
||||
<div class="item-name">${frappe.utils.escape_html(item_data.item_name)}</div>
|
||||
<div class="item-qty">${item_data.qty || 0} ${frappe.utils.escape_html(item_data.uom)}</div>
|
||||
<div class="item-rate-disc">${get_rate_discount_html()}</div>
|
||||
</div>`;
|
||||
|
||||
@@ -139,10 +143,10 @@ erpnext.PointOfSale.PastOrderSummary = class {
|
||||
.map((t) => {
|
||||
// if tax rate is 0, don't print it.
|
||||
const description = /[0-9]+/.test(t.description)
|
||||
? t.description
|
||||
? frappe.utils.escape_html(t.description)
|
||||
: t.rate != 0
|
||||
? `${t.description} @ ${t.rate}%`
|
||||
: t.description;
|
||||
? `${frappe.utils.escape_html(t.description)} @ ${t.rate}%`
|
||||
: frappe.utils.escape_html(t.description);
|
||||
return `
|
||||
<div class="tax-row">
|
||||
<div class="tax-label">${description}</div>
|
||||
|
||||
@@ -408,8 +408,10 @@ erpnext.PointOfSale.Payment = class {
|
||||
|
||||
return `
|
||||
<div class="payment-mode-wrapper">
|
||||
<div class="mode-of-payment" data-mode="${mode}" data-payment-type="${payment_type}">
|
||||
${p.mode_of_payment}
|
||||
<div class="mode-of-payment" data-mode="${mode}" data-payment-type="${frappe.utils.escape_html(
|
||||
payment_type
|
||||
)}">
|
||||
${frappe.utils.escape_html(p.mode_of_payment)}
|
||||
<div class="${mode}-amount pay-amount">${amount}</div>
|
||||
<div class="${mode} mode-of-payment-control"></div>
|
||||
</div>
|
||||
@@ -544,7 +546,7 @@ erpnext.PointOfSale.Payment = class {
|
||||
<div class="mode-of-payment loyalty-card" data-mode="loyalty-amount" data-payment-type="loyalty-amount">
|
||||
Redeem Loyalty Points
|
||||
<div class="loyalty-amount-amount pay-amount">${amount}</div>
|
||||
<div class="loyalty-amount-name">${loyalty_program}</div>
|
||||
<div class="loyalty-amount-name">${frappe.utils.escape_html(loyalty_program)}</div>
|
||||
<div class="loyalty-amount mode-of-payment-control"></div>
|
||||
</div>
|
||||
</div>`
|
||||
|
||||
@@ -11,9 +11,9 @@ field_map = {
|
||||
"name",
|
||||
"address_line1",
|
||||
"address_line2",
|
||||
"pincode",
|
||||
"city",
|
||||
"state",
|
||||
"pincode",
|
||||
"country",
|
||||
"is_primary_address",
|
||||
],
|
||||
|
||||
@@ -138,12 +138,30 @@ class Analytics:
|
||||
self.get_sales_transactions_based_on_project()
|
||||
self.get_rows()
|
||||
|
||||
def _get_permitted_parent_names(self):
|
||||
return frappe.get_list(
|
||||
self.filters.doc_type,
|
||||
fields=["name"],
|
||||
filters={
|
||||
"docstatus": 1,
|
||||
"company": ["in", self.filters.company],
|
||||
self.date_field: ("between", [self.filters.from_date, self.filters.to_date]),
|
||||
},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
def get_sales_transactions_based_on_order_type(self):
|
||||
if self.filters["value_quantity"] == "Value":
|
||||
value_field = "base_net_total"
|
||||
else:
|
||||
value_field = "total_qty"
|
||||
|
||||
permitted_names = self._get_permitted_parent_names()
|
||||
if not permitted_names:
|
||||
self.entries = []
|
||||
self.get_teams()
|
||||
return
|
||||
|
||||
doctype = DocType(self.filters.doc_type)
|
||||
|
||||
self.entries = (
|
||||
@@ -153,12 +171,7 @@ class Analytics:
|
||||
doctype[self.date_field],
|
||||
doctype[value_field].as_("value_field"),
|
||||
)
|
||||
.where(
|
||||
(doctype.docstatus == 1)
|
||||
& (doctype.company.isin(self.filters.company))
|
||||
& (doctype[self.date_field].between(self.filters.from_date, self.filters.to_date))
|
||||
& (IfNull(doctype.order_type, "") != "")
|
||||
)
|
||||
.where((doctype.name.isin(permitted_names)) & (IfNull(doctype.order_type, "") != ""))
|
||||
.orderby(doctype.order_type)
|
||||
).run(as_dict=True)
|
||||
|
||||
@@ -186,8 +199,10 @@ class Analytics:
|
||||
if self.filters.doc_type in ["Sales Invoice", "Purchase Invoice", "Payment Entry"]:
|
||||
filters.update({"is_opening": "No"})
|
||||
|
||||
self.entries = frappe.get_all(
|
||||
self.filters.doc_type, fields=[entity, entity_name, value_field, self.date_field], filters=filters
|
||||
self.entries = frappe.get_list(
|
||||
self.filters.doc_type,
|
||||
fields=[entity, entity_name, value_field, self.date_field],
|
||||
filters=filters,
|
||||
)
|
||||
|
||||
self.entity_names = {}
|
||||
@@ -200,6 +215,12 @@ class Analytics:
|
||||
else:
|
||||
value_field = "stock_qty"
|
||||
|
||||
permitted_names = self._get_permitted_parent_names()
|
||||
if not permitted_names:
|
||||
self.entries = []
|
||||
self.entity_names = {}
|
||||
return
|
||||
|
||||
doctype = DocType(self.filters.doc_type)
|
||||
doctype_item = DocType(f"{self.filters.doc_type} Item")
|
||||
|
||||
@@ -214,11 +235,7 @@ class Analytics:
|
||||
doctype_item[value_field].as_("value_field"),
|
||||
doctype[self.date_field],
|
||||
)
|
||||
.where(
|
||||
(doctype_item.docstatus == 1)
|
||||
& (doctype.company.isin(self.filters.company))
|
||||
& (doctype[self.date_field].between(self.filters.from_date, self.filters.to_date))
|
||||
)
|
||||
.where((doctype_item.docstatus == 1) & (doctype.name.isin(permitted_names)))
|
||||
).run(as_dict=True)
|
||||
|
||||
self.entity_names = {}
|
||||
@@ -248,7 +265,7 @@ class Analytics:
|
||||
if self.filters.doc_type in ["Sales Invoice", "Purchase Invoice", "Payment Entry"]:
|
||||
filters.update({"is_opening": "No"})
|
||||
|
||||
self.entries = frappe.get_all(
|
||||
self.entries = frappe.get_list(
|
||||
self.filters.doc_type,
|
||||
fields=[entity_field, value_field, self.date_field],
|
||||
filters=filters,
|
||||
@@ -261,6 +278,12 @@ class Analytics:
|
||||
else:
|
||||
value_field = "qty"
|
||||
|
||||
permitted_names = self._get_permitted_parent_names()
|
||||
if not permitted_names:
|
||||
self.entries = []
|
||||
self.get_groups()
|
||||
return
|
||||
|
||||
doctype = DocType(self.filters.doc_type)
|
||||
doctype_item = DocType(f"{self.filters.doc_type} Item")
|
||||
|
||||
@@ -273,11 +296,7 @@ class Analytics:
|
||||
doctype_item[value_field].as_("value_field"),
|
||||
doctype[self.date_field],
|
||||
)
|
||||
.where(
|
||||
(doctype_item.docstatus == 1)
|
||||
& (doctype.company.isin(self.filters.company))
|
||||
& (doctype[self.date_field].between(self.filters.from_date, self.filters.to_date))
|
||||
)
|
||||
.where((doctype_item.docstatus == 1) & (doctype.name.isin(permitted_names)))
|
||||
).run(as_dict=True)
|
||||
|
||||
self.get_groups()
|
||||
@@ -300,8 +319,10 @@ class Analytics:
|
||||
if self.filters.doc_type in ["Sales Invoice", "Purchase Invoice", "Payment Entry"]:
|
||||
filters.update({"is_opening": "No"})
|
||||
|
||||
self.entries = frappe.get_all(
|
||||
self.filters.doc_type, fields=[entity, value_field, self.date_field], filters=filters
|
||||
self.entries = frappe.get_list(
|
||||
self.filters.doc_type,
|
||||
fields=[entity, value_field, self.date_field],
|
||||
filters=filters,
|
||||
)
|
||||
|
||||
def get_rows(self):
|
||||
|
||||
@@ -77,13 +77,11 @@ class CustomerGroup(NestedSet):
|
||||
|
||||
def get_parent_customer_groups(customer_group):
|
||||
lft, rgt = frappe.db.get_value("Customer Group", customer_group, ["lft", "rgt"])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""select name from `tabCustomer Group`
|
||||
where lft <= %s and rgt >= %s
|
||||
order by lft asc""",
|
||||
(lft, rgt),
|
||||
as_dict=True,
|
||||
return frappe.get_all(
|
||||
"Customer Group",
|
||||
filters=[["lft", "<=", lft], ["rgt", ">=", rgt]],
|
||||
fields=["name"],
|
||||
order_by="lft asc",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -70,3 +70,13 @@ class SupplierGroup(NestedSet):
|
||||
def on_trash(self):
|
||||
NestedSet.validate_if_child_exists(self)
|
||||
frappe.utils.nestedset.update_nsm(self)
|
||||
|
||||
|
||||
def get_parent_supplier_groups(supplier_group):
|
||||
lft, rgt = frappe.db.get_value("Supplier Group", supplier_group, ["lft", "rgt"])
|
||||
return frappe.get_all(
|
||||
"Supplier Group",
|
||||
filters=[["lft", "<=", lft], ["rgt", ">=", rgt]],
|
||||
fields=["name"],
|
||||
order_by="lft asc",
|
||||
)
|
||||
|
||||
@@ -24,6 +24,7 @@ def after_install():
|
||||
|
||||
set_single_defaults()
|
||||
create_print_setting_custom_fields()
|
||||
create_address_and_contact_custom_fields()
|
||||
create_custom_company_links()
|
||||
add_all_roles_to("Administrator")
|
||||
create_default_success_action()
|
||||
@@ -132,6 +133,37 @@ def create_print_setting_custom_fields():
|
||||
)
|
||||
|
||||
|
||||
def create_address_and_contact_custom_fields():
|
||||
create_custom_fields(
|
||||
{
|
||||
"Address": [
|
||||
{
|
||||
"label": _("Tax Category"),
|
||||
"fieldname": "tax_category",
|
||||
"fieldtype": "Link",
|
||||
"options": "Tax Category",
|
||||
"insert_after": "fax",
|
||||
},
|
||||
{
|
||||
"label": _("Is Your Company Address"),
|
||||
"fieldname": "is_your_company_address",
|
||||
"fieldtype": "Check",
|
||||
"default": "0",
|
||||
"insert_after": "linked_with",
|
||||
},
|
||||
],
|
||||
"Contact": [
|
||||
{
|
||||
"label": _("Is Billing Contact"),
|
||||
"fieldname": "is_billing_contact",
|
||||
"fieldtype": "Check",
|
||||
"insert_after": "is_primary_contact",
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def create_custom_company_links():
|
||||
"""Add link fields to Company in Email Account and Communication.
|
||||
|
||||
|
||||
@@ -262,7 +262,15 @@
|
||||
},
|
||||
"Belgium VAT 12%": {
|
||||
"account_name": "VAT 12%",
|
||||
"tax_rate": 12
|
||||
"tax_rate": 12.00
|
||||
},
|
||||
"Belgium VAT 6%": {
|
||||
"account_name": "VAT 6%",
|
||||
"tax_rate": 6.00
|
||||
},
|
||||
"Belgium VAT 0%": {
|
||||
"account_name": "VAT 0%",
|
||||
"tax_rate": 0.00
|
||||
}
|
||||
},
|
||||
|
||||
@@ -4115,9 +4123,14 @@
|
||||
},
|
||||
|
||||
"Japan": {
|
||||
"Japan Tax": {
|
||||
"account_name": "CT",
|
||||
"tax_rate": 5.00
|
||||
"Japan Tax 10%": {
|
||||
"account_name": "CT 10%",
|
||||
"tax_rate": 10.00,
|
||||
"default": 1
|
||||
},
|
||||
"Japan Tax 8%": {
|
||||
"account_name": "CT 8%",
|
||||
"tax_rate": 8.00
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -30,11 +30,20 @@ class DeprecatedSerialNoValuation:
|
||||
def get_incoming_value_for_serial_nos(self, serial_nos):
|
||||
from erpnext.stock.utils import get_combine_datetime
|
||||
|
||||
do_not_fetch_rate = frappe.db.get_single_value(
|
||||
"Stock Reposting Settings", "do_not_fetch_incoming_rate_from_serial_no"
|
||||
)
|
||||
|
||||
# get rate from serial nos within same company
|
||||
incoming_values = 0.0
|
||||
for serial_no in serial_nos:
|
||||
sn_details = frappe.db.get_value("Serial No", serial_no, ["purchase_rate", "company"], as_dict=1)
|
||||
if sn_details and sn_details.purchase_rate and sn_details.company == self.sle.company:
|
||||
if (
|
||||
sn_details
|
||||
and sn_details.purchase_rate
|
||||
and sn_details.company == self.sle.company
|
||||
and (not frappe.flags.through_repost_item_valuation or not do_not_fetch_rate)
|
||||
):
|
||||
self.serial_no_incoming_rate[serial_no] += flt(sn_details.purchase_rate)
|
||||
incoming_values += self.serial_no_incoming_rate[serial_no]
|
||||
continue
|
||||
|
||||
@@ -11,7 +11,6 @@ from frappe.model.naming import make_autoname, revert_series_if_last
|
||||
from frappe.query_builder.functions import CurDate, Sum
|
||||
from frappe.utils import cint, flt, get_link_to_form
|
||||
from frappe.utils.data import add_days
|
||||
from frappe.utils.jinja import render_template
|
||||
|
||||
|
||||
class UnableToSelectBatchError(frappe.ValidationError):
|
||||
@@ -225,10 +224,8 @@ class Batch(Document):
|
||||
:return: The string that was generated.
|
||||
"""
|
||||
naming_series_prefix = _get_batch_prefix()
|
||||
# validate_template(naming_series_prefix)
|
||||
naming_series_prefix = render_template(str(naming_series_prefix), self.__dict__)
|
||||
key = _make_naming_series_key(naming_series_prefix)
|
||||
name = make_autoname(key)
|
||||
name = make_autoname(key, doc=self)
|
||||
|
||||
return name
|
||||
|
||||
|
||||
@@ -506,6 +506,24 @@ class TestBatch(FrappeTestCase):
|
||||
if not use_naming_series:
|
||||
frappe.set_value("Stock Settings", "Stock Settings", "use_naming_series", 0)
|
||||
|
||||
def test_naming_series_prefix_is_not_rendered_as_jinja(self):
|
||||
from frappe.model.naming import InvalidNamingSeriesError
|
||||
|
||||
stock_settings = frappe.get_single("Stock Settings")
|
||||
use_naming_series = cint(stock_settings.use_naming_series)
|
||||
original_prefix = stock_settings.naming_series_prefix
|
||||
|
||||
frappe.set_value("Stock Settings", "Stock Settings", "use_naming_series", 1)
|
||||
frappe.set_value("Stock Settings", "Stock Settings", "naming_series_prefix", "{{ 7*7 }}")
|
||||
|
||||
try:
|
||||
self.assertRaises(
|
||||
InvalidNamingSeriesError, self.make_new_batch, "_Test Stock Item For Batch SSTI"
|
||||
)
|
||||
finally:
|
||||
frappe.set_value("Stock Settings", "Stock Settings", "naming_series_prefix", original_prefix)
|
||||
frappe.set_value("Stock Settings", "Stock Settings", "use_naming_series", use_naming_series)
|
||||
|
||||
def make_new_batch(self, item_name=None, batch_id=None, do_not_insert=0):
|
||||
batch = frappe.new_doc("Batch")
|
||||
item = self.make_batch_item(item_name)
|
||||
|
||||
@@ -263,8 +263,9 @@ def update_qty(bin_name, args):
|
||||
# actual qty is already updated by processing current voucher
|
||||
actual_qty = bin_details.actual_qty or 0.0
|
||||
|
||||
# actual qty is not up to date in case of backdated transaction
|
||||
if future_sle_exists(args):
|
||||
# actual qty is not up to date in case of backdated transactions
|
||||
# or when cancellations are the most recent SLE
|
||||
if future_sle_exists(args) or args.get("is_cancelled"):
|
||||
actual_qty = get_actual_qty(args.get("item_code"), args.get("warehouse"))
|
||||
|
||||
ordered_qty = flt(bin_details.ordered_qty) + flt(args.get("ordered_qty"))
|
||||
|
||||
@@ -15,7 +15,7 @@ from frappe.query_builder import DocType
|
||||
from frappe.query_builder.functions import Abs, Sum
|
||||
from frappe.utils import cint, flt
|
||||
|
||||
from erpnext.accounts.party import get_due_date
|
||||
from erpnext.accounts.party import CROSS_PARTY_FIELD_NO_MAP, get_due_date
|
||||
from erpnext.controllers.accounts_controller import get_taxes_and_charges, merge_taxes
|
||||
from erpnext.controllers.selling_controller import SellingController
|
||||
from erpnext.stock.stock_ledger import validate_reserved_stock
|
||||
@@ -1405,8 +1405,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
doctype: {
|
||||
"doctype": target_doctype,
|
||||
"postprocess": update_details,
|
||||
"field_no_map": ["taxes_and_charges", "set_warehouse"],
|
||||
"field_map": {"shipping_address_name": "shipping_address"},
|
||||
"field_no_map": [*CROSS_PARTY_FIELD_NO_MAP, "set_warehouse"],
|
||||
},
|
||||
doctype + " Item": {
|
||||
"doctype": target_doctype + " Item",
|
||||
|
||||
@@ -226,13 +226,6 @@ frappe.ui.form.on("Item", {
|
||||
});
|
||||
frm.set_df_property("is_fixed_asset", "read_only", frm.doc.__onload?.asset_exists ? 1 : 0);
|
||||
frm.toggle_reqd("customer", frm.doc.is_customer_provided_item ? 1 : 0);
|
||||
frm.set_query("item_group", () => {
|
||||
return {
|
||||
filters: {
|
||||
is_group: 0,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
validate: function (frm) {
|
||||
@@ -411,12 +404,6 @@ $.extend(erpnext.item, {
|
||||
};
|
||||
};
|
||||
|
||||
frm.fields_dict["item_group"].get_query = function (doc, cdt, cdn) {
|
||||
return {
|
||||
filters: [["Item Group", "docstatus", "!=", 2]],
|
||||
};
|
||||
};
|
||||
|
||||
frm.fields_dict["item_defaults"].grid.get_field("deferred_revenue_account").get_query = function (
|
||||
doc,
|
||||
cdt,
|
||||
@@ -594,11 +581,10 @@ $.extend(erpnext.item, {
|
||||
default: 0,
|
||||
onchange: function () {
|
||||
let selected_attributes = get_selected_attributes();
|
||||
let lengths = [];
|
||||
Object.keys(selected_attributes).map((key) => {
|
||||
lengths.push(selected_attributes[key].length);
|
||||
let lengths = Object.keys(selected_attributes).map((key) => {
|
||||
return selected_attributes[key].length;
|
||||
});
|
||||
if (lengths.includes(0)) {
|
||||
if (!lengths.length) {
|
||||
me.multiple_variant_dialog.get_primary_btn().html(__("Create Variants"));
|
||||
me.multiple_variant_dialog.disable_primary_action();
|
||||
} else {
|
||||
@@ -635,7 +621,7 @@ $.extend(erpnext.item, {
|
||||
fieldtype: "HTML",
|
||||
fieldname: "help",
|
||||
options: `<label class="control-label">
|
||||
${__("Select at least one value from each of the attributes.")}
|
||||
${__("Select at least one attribute value.")}
|
||||
</label>`,
|
||||
},
|
||||
]
|
||||
@@ -693,6 +679,9 @@ $.extend(erpnext.item, {
|
||||
selected_attributes[attribute_name].push($(opt).attr("data-fieldname"));
|
||||
}
|
||||
});
|
||||
if (!selected_attributes[attribute_name].length) {
|
||||
delete selected_attributes[attribute_name];
|
||||
}
|
||||
});
|
||||
|
||||
return selected_attributes;
|
||||
@@ -751,14 +740,18 @@ $.extend(erpnext.item, {
|
||||
|
||||
if (!row.disabled) {
|
||||
if (row.numeric_values) {
|
||||
fieldtype = "Float";
|
||||
desc =
|
||||
"Min Value: " +
|
||||
row.from_range +
|
||||
" , Max Value: " +
|
||||
row.to_range +
|
||||
", in Increments of: " +
|
||||
row.increment;
|
||||
const all_are_int =
|
||||
flt(row.from_range) === cint(row.from_range) &&
|
||||
flt(row.to_range) === cint(row.to_range) &&
|
||||
flt(row.increment) === cint(row.increment);
|
||||
fieldtype = all_are_int ? "Int" : "Float";
|
||||
const df = { fieldtype };
|
||||
const options = all_are_int ? { inline: 1 } : { always_show_decimals: true, inline: 1 };
|
||||
desc = __("Min Value: {0}, Max Value: {1}, in Increments of: {2}", [
|
||||
frappe.format(row.from_range, df, options),
|
||||
frappe.format(row.to_range, df, options),
|
||||
frappe.format(row.increment, df, options),
|
||||
]);
|
||||
} else {
|
||||
fieldtype = "Data";
|
||||
desc = "";
|
||||
|
||||
@@ -855,8 +855,13 @@ class Item(Document):
|
||||
if disabled:
|
||||
frappe.throw(_("Attribute {0} is disabled.").format(frappe.bold(d.attribute)))
|
||||
|
||||
if not numeric_values and not frappe.db.exists(
|
||||
"Item Attribute Value", {"parent": d.attribute, "attribute_value": d.attribute_value}
|
||||
if (
|
||||
not numeric_values
|
||||
and d.attribute_value
|
||||
and not frappe.db.exists(
|
||||
"Item Attribute Value",
|
||||
{"parent": d.attribute, "attribute_value": d.attribute_value},
|
||||
)
|
||||
):
|
||||
frappe.throw(
|
||||
_("Attribute Value {0} is not valid for the selected attribute {1}.").format(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_bulk_edit": 1,
|
||||
"creation": "2014-07-11 11:51:00.453717",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
@@ -12,7 +13,10 @@
|
||||
"col_break3",
|
||||
"amount",
|
||||
"base_amount",
|
||||
"has_corrective_cost"
|
||||
"has_corrective_cost",
|
||||
"has_operating_cost",
|
||||
"operation_id",
|
||||
"qty"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -70,12 +74,36 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Has Corrective Cost",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "has_operating_cost",
|
||||
"fieldtype": "Check",
|
||||
"label": "Has Operating Cost",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "operation_id",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Operation ID",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "qty",
|
||||
"fieldtype": "Float",
|
||||
"hidden": 1,
|
||||
"label": "Qty",
|
||||
"no_copy": 1,
|
||||
"non_negative": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2025-01-20 12:22:03.455762",
|
||||
"modified": "2026-05-19 12:21:07.953801",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Landed Cost Taxes and Charges",
|
||||
@@ -83,4 +111,4 @@
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user