mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-19 04:59:18 +00:00
Merge branch 'develop' into fixed-transferred-materials-are-not-consumed
This commit is contained in:
12
.github/stale.yml
vendored
12
.github/stale.yml
vendored
@@ -24,14 +24,4 @@ pulls:
|
|||||||
:) Also, even if it is closed, you can always reopen the PR when you're
|
:) Also, even if it is closed, you can always reopen the PR when you're
|
||||||
ready. Thank you for contributing.
|
ready. Thank you for contributing.
|
||||||
|
|
||||||
issues:
|
only: pulls
|
||||||
daysUntilStale: 90
|
|
||||||
daysUntilClose: 7
|
|
||||||
exemptLabels:
|
|
||||||
- valid
|
|
||||||
- to-validate
|
|
||||||
- QA
|
|
||||||
markComment: >
|
|
||||||
This issue has been automatically marked as inactive because it has not had
|
|
||||||
recent activity and it wasn't validated by maintainer team. It will be
|
|
||||||
closed within a week if no further activity occurs.
|
|
||||||
|
|||||||
1
.github/workflows/patch.yml
vendored
1
.github/workflows/patch.yml
vendored
@@ -115,4 +115,5 @@ jobs:
|
|||||||
echo "Updating to latest version"
|
echo "Updating to latest version"
|
||||||
git -C "apps/frappe" checkout -q -f "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}"
|
git -C "apps/frappe" checkout -q -f "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}"
|
||||||
git -C "apps/erpnext" checkout -q -f "$GITHUB_SHA"
|
git -C "apps/erpnext" checkout -q -f "$GITHUB_SHA"
|
||||||
|
bench setup requirements --python
|
||||||
bench --site test_site migrate
|
bench --site test_site migrate
|
||||||
|
|||||||
12
.mergify.yml
12
.mergify.yml
@@ -9,6 +9,8 @@ pull_request_rules:
|
|||||||
- author!=nabinhait
|
- author!=nabinhait
|
||||||
- author!=ankush
|
- author!=ankush
|
||||||
- author!=deepeshgarg007
|
- author!=deepeshgarg007
|
||||||
|
- author!=mergify[bot]
|
||||||
|
|
||||||
- or:
|
- or:
|
||||||
- base=version-13
|
- base=version-13
|
||||||
- base=version-12
|
- base=version-12
|
||||||
@@ -19,6 +21,16 @@ pull_request_rules:
|
|||||||
@{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch.
|
@{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch.
|
||||||
https://github.com/frappe/erpnext/wiki/Pull-Request-Checklist#which-branch
|
https://github.com/frappe/erpnext/wiki/Pull-Request-Checklist#which-branch
|
||||||
|
|
||||||
|
- name: Auto-close PRs on pre-release branch
|
||||||
|
conditions:
|
||||||
|
- base=version-13-pre-release
|
||||||
|
actions:
|
||||||
|
close:
|
||||||
|
comment:
|
||||||
|
message: |
|
||||||
|
@{{author}}, pre-release branch is not maintained anymore. Releases are directly done by merging hotfix branch to stable branches.
|
||||||
|
|
||||||
|
|
||||||
- name: backport to develop
|
- name: backport to develop
|
||||||
conditions:
|
conditions:
|
||||||
- label="backport develop"
|
- label="backport develop"
|
||||||
|
|||||||
@@ -322,9 +322,9 @@ def get_parent_account(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
return frappe.db.sql(
|
return frappe.db.sql(
|
||||||
"""select name from tabAccount
|
"""select name from tabAccount
|
||||||
where is_group = 1 and docstatus != 2 and company = %s
|
where is_group = 1 and docstatus != 2 and company = %s
|
||||||
and %s like %s order by name limit %s, %s"""
|
and %s like %s order by name limit %s offset %s"""
|
||||||
% ("%s", searchfield, "%s", "%s", "%s"),
|
% ("%s", searchfield, "%s", "%s", "%s"),
|
||||||
(filters["company"], "%%%s%%" % txt, start, page_len),
|
(filters["company"], "%%%s%%" % txt, page_len, start),
|
||||||
as_list=1,
|
as_list=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1239,7 +1239,7 @@ def get_against_jv(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
AND jv.docstatus = 1
|
AND jv.docstatus = 1
|
||||||
AND jv.`{0}` LIKE %(txt)s
|
AND jv.`{0}` LIKE %(txt)s
|
||||||
ORDER BY jv.name DESC
|
ORDER BY jv.name DESC
|
||||||
LIMIT %(offset)s, %(limit)s
|
LIMIT %(limit)s offset %(offset)s
|
||||||
""".format(
|
""".format(
|
||||||
searchfield
|
searchfield
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"actions": [],
|
"actions": [],
|
||||||
"allow_rename": 1,
|
"allow_rename": 1,
|
||||||
"autoname": "format:PLE-{YY}-{MM}-{######}",
|
|
||||||
"creation": "2022-05-09 19:35:03.334361",
|
"creation": "2022-05-09 19:35:03.334361",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
@@ -138,11 +137,10 @@
|
|||||||
"in_create": 1,
|
"in_create": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-05-19 18:04:44.609115",
|
"modified": "2022-05-30 19:04:55.532171",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Ledger Entry",
|
"name": "Payment Ledger Entry",
|
||||||
"naming_rule": "Expression",
|
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ def get_mop_query(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
return frappe.db.sql(
|
return frappe.db.sql(
|
||||||
""" select mode_of_payment from `tabPayment Order Reference`
|
""" select mode_of_payment from `tabPayment Order Reference`
|
||||||
where parent = %(parent)s and mode_of_payment like %(txt)s
|
where parent = %(parent)s and mode_of_payment like %(txt)s
|
||||||
limit %(start)s, %(page_len)s""",
|
limit %(page_len)s offset %(start)s""",
|
||||||
{"parent": filters.get("parent"), "start": start, "page_len": page_len, "txt": "%%%s%%" % txt},
|
{"parent": filters.get("parent"), "start": start, "page_len": page_len, "txt": "%%%s%%" % txt},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ def get_supplier_query(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
""" select supplier from `tabPayment Order Reference`
|
""" select supplier from `tabPayment Order Reference`
|
||||||
where parent = %(parent)s and supplier like %(txt)s and
|
where parent = %(parent)s and supplier like %(txt)s and
|
||||||
(payment_reference is null or payment_reference='')
|
(payment_reference is null or payment_reference='')
|
||||||
limit %(start)s, %(page_len)s""",
|
limit %(page_len)s offset %(start)s""",
|
||||||
{"parent": filters.get("parent"), "start": start, "page_len": page_len, "txt": "%%%s%%" % txt},
|
{"parent": filters.get("parent"), "start": start, "page_len": page_len, "txt": "%%%s%%" % txt},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -54,8 +54,8 @@ class PeriodClosingVoucher(AccountsController):
|
|||||||
|
|
||||||
pce = frappe.db.sql(
|
pce = frappe.db.sql(
|
||||||
"""select name from `tabPeriod Closing Voucher`
|
"""select name from `tabPeriod Closing Voucher`
|
||||||
where posting_date > %s and fiscal_year = %s and docstatus = 1""",
|
where posting_date > %s and fiscal_year = %s and docstatus = 1 and company = %s""",
|
||||||
(self.posting_date, self.fiscal_year),
|
(self.posting_date, self.fiscal_year, self.company),
|
||||||
)
|
)
|
||||||
if pce and pce[0][0]:
|
if pce and pce[0][0]:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ def pos_profile_query(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
where
|
where
|
||||||
pfu.parent = pf.name and pfu.user = %(user)s and pf.company = %(company)s
|
pfu.parent = pf.name and pfu.user = %(user)s and pf.company = %(company)s
|
||||||
and (pf.name like %(txt)s)
|
and (pf.name like %(txt)s)
|
||||||
and pf.disabled = 0 limit %(start)s, %(page_len)s""",
|
and pf.disabled = 0 limit %(page_len)s offset %(start)s""",
|
||||||
args,
|
args,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -45,8 +45,6 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
|||||||
if (this.frm.doc.supplier && this.frm.doc.__islocal) {
|
if (this.frm.doc.supplier && this.frm.doc.__islocal) {
|
||||||
this.frm.trigger('supplier');
|
this.frm.trigger('supplier');
|
||||||
}
|
}
|
||||||
|
|
||||||
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh(doc) {
|
refresh(doc) {
|
||||||
|
|||||||
@@ -545,7 +545,16 @@ class PurchaseInvoice(BuyingController):
|
|||||||
from_repost=from_repost,
|
from_repost=from_repost,
|
||||||
)
|
)
|
||||||
elif self.docstatus == 2:
|
elif self.docstatus == 2:
|
||||||
|
provisional_entries = [a for a in gl_entries if a.voucher_type == "Purchase Receipt"]
|
||||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||||
|
if provisional_entries:
|
||||||
|
for entry in provisional_entries:
|
||||||
|
frappe.db.set_value(
|
||||||
|
"GL Entry",
|
||||||
|
{"voucher_type": "Purchase Receipt", "voucher_detail_no": entry.voucher_detail_no},
|
||||||
|
"is_cancelled",
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
|
||||||
if update_outstanding == "No":
|
if update_outstanding == "No":
|
||||||
update_outstanding_amt(
|
update_outstanding_amt(
|
||||||
@@ -1127,7 +1136,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
# Stock ledger value is not matching with the warehouse amount
|
# Stock ledger value is not matching with the warehouse amount
|
||||||
if (
|
if (
|
||||||
self.update_stock
|
self.update_stock
|
||||||
and voucher_wise_stock_value.get(item.name)
|
and voucher_wise_stock_value.get((item.name, item.warehouse))
|
||||||
and warehouse_debit_amount
|
and warehouse_debit_amount
|
||||||
!= flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision)
|
!= flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision)
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -27,12 +27,13 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
|
|||||||
make_purchase_receipt,
|
make_purchase_receipt,
|
||||||
)
|
)
|
||||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import get_qty_after_transaction
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import get_qty_after_transaction
|
||||||
|
from erpnext.stock.tests.test_utils import StockTestMixin
|
||||||
|
|
||||||
test_dependencies = ["Item", "Cost Center", "Payment Term", "Payment Terms Template"]
|
test_dependencies = ["Item", "Cost Center", "Payment Term", "Payment Terms Template"]
|
||||||
test_ignore = ["Serial No"]
|
test_ignore = ["Serial No"]
|
||||||
|
|
||||||
|
|
||||||
class TestPurchaseInvoice(unittest.TestCase):
|
class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(self):
|
def setUpClass(self):
|
||||||
unlink_payment_on_cancel_of_invoice()
|
unlink_payment_on_cancel_of_invoice()
|
||||||
@@ -693,6 +694,80 @@ class TestPurchaseInvoice(unittest.TestCase):
|
|||||||
self.assertEqual(expected_values[gle.account][0], gle.debit)
|
self.assertEqual(expected_values[gle.account][0], gle.debit)
|
||||||
self.assertEqual(expected_values[gle.account][1], gle.credit)
|
self.assertEqual(expected_values[gle.account][1], gle.credit)
|
||||||
|
|
||||||
|
def test_standalone_return_using_pi(self):
|
||||||
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||||
|
|
||||||
|
item = self.make_item().name
|
||||||
|
company = "_Test Company with perpetual inventory"
|
||||||
|
warehouse = "Stores - TCP1"
|
||||||
|
|
||||||
|
make_stock_entry(item_code=item, target=warehouse, qty=50, rate=120)
|
||||||
|
|
||||||
|
return_pi = make_purchase_invoice(
|
||||||
|
is_return=1,
|
||||||
|
item=item,
|
||||||
|
qty=-10,
|
||||||
|
update_stock=1,
|
||||||
|
rate=100,
|
||||||
|
company=company,
|
||||||
|
warehouse=warehouse,
|
||||||
|
cost_center="Main - TCP1",
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert that stock consumption is with actual rate
|
||||||
|
self.assertGLEs(
|
||||||
|
return_pi,
|
||||||
|
[{"credit": 1200, "debit": 0}],
|
||||||
|
gle_filters={"account": "Stock In Hand - TCP1"},
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert loss booked in COGS
|
||||||
|
self.assertGLEs(
|
||||||
|
return_pi,
|
||||||
|
[{"credit": 0, "debit": 200}],
|
||||||
|
gle_filters={"account": "Cost of Goods Sold - TCP1"},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_return_with_lcv(self):
|
||||||
|
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||||
|
from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
|
||||||
|
create_landed_cost_voucher,
|
||||||
|
)
|
||||||
|
|
||||||
|
item = self.make_item().name
|
||||||
|
company = "_Test Company with perpetual inventory"
|
||||||
|
warehouse = "Stores - TCP1"
|
||||||
|
cost_center = "Main - TCP1"
|
||||||
|
|
||||||
|
pi = make_purchase_invoice(
|
||||||
|
item=item,
|
||||||
|
company=company,
|
||||||
|
warehouse=warehouse,
|
||||||
|
cost_center=cost_center,
|
||||||
|
update_stock=1,
|
||||||
|
qty=10,
|
||||||
|
rate=100,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create landed cost voucher - will increase valuation of received item by 10
|
||||||
|
create_landed_cost_voucher("Purchase Invoice", pi.name, pi.company, charges=100)
|
||||||
|
return_pi = make_return_doc(pi.doctype, pi.name)
|
||||||
|
return_pi.save().submit()
|
||||||
|
|
||||||
|
# assert that stock consumption is with actual in rate
|
||||||
|
self.assertGLEs(
|
||||||
|
return_pi,
|
||||||
|
[{"credit": 1100, "debit": 0}],
|
||||||
|
gle_filters={"account": "Stock In Hand - TCP1"},
|
||||||
|
)
|
||||||
|
|
||||||
|
# assert loss booked in COGS
|
||||||
|
self.assertGLEs(
|
||||||
|
return_pi,
|
||||||
|
[{"credit": 0, "debit": 100}],
|
||||||
|
gle_filters={"account": "Cost of Goods Sold - TCP1"},
|
||||||
|
)
|
||||||
|
|
||||||
def test_multi_currency_gle(self):
|
def test_multi_currency_gle(self):
|
||||||
pi = make_purchase_invoice(
|
pi = make_purchase_invoice(
|
||||||
supplier="_Test Supplier USD",
|
supplier="_Test Supplier USD",
|
||||||
@@ -1526,6 +1601,18 @@ class TestPurchaseInvoice(unittest.TestCase):
|
|||||||
|
|
||||||
check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date)
|
check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date)
|
||||||
|
|
||||||
|
# Cancel purchase invoice to check reverse provisional entry cancellation
|
||||||
|
pi.cancel()
|
||||||
|
|
||||||
|
expected_gle_for_purchase_receipt_post_pi_cancel = [
|
||||||
|
["Provision Account - _TC", 0, 250, pi.posting_date],
|
||||||
|
["_Test Account Cost for Goods Sold - _TC", 250, 0, pi.posting_date],
|
||||||
|
]
|
||||||
|
|
||||||
|
check_gl_entries(
|
||||||
|
self, pr.name, expected_gle_for_purchase_receipt_post_pi_cancel, pr.posting_date
|
||||||
|
)
|
||||||
|
|
||||||
company.enable_provisional_accounting_for_non_stock_items = 0
|
company.enable_provisional_accounting_for_non_stock_items = 0
|
||||||
company.save()
|
company.save()
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
|||||||
me.frm.refresh_fields();
|
me.frm.refresh_fields();
|
||||||
}
|
}
|
||||||
erpnext.queries.setup_warehouse_query(this.frm);
|
erpnext.queries.setup_warehouse_query(this.frm);
|
||||||
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh(doc, dt, dn) {
|
refresh(doc, dt, dn) {
|
||||||
|
|||||||
@@ -1790,6 +1790,8 @@
|
|||||||
"width": "50%"
|
"width": "50%"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"fetch_from": "sales_partner.commission_rate",
|
||||||
|
"fetch_if_empty": 1,
|
||||||
"fieldname": "commission_rate",
|
"fieldname": "commission_rate",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"hide_days": 1,
|
"hide_days": 1,
|
||||||
@@ -2038,7 +2040,7 @@
|
|||||||
"link_fieldname": "consolidated_invoice"
|
"link_fieldname": "consolidated_invoice"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2022-03-08 16:08:53.517903",
|
"modified": "2022-06-10 03:52:51.409913",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice",
|
"name": "Sales Invoice",
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/accounts",
|
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/accounts",
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"is_complete": 0,
|
"is_complete": 0,
|
||||||
"modified": "2022-01-18 18:35:52.326688",
|
"modified": "2022-06-07 14:29:21.352132",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Accounts",
|
"name": "Accounts",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"action": "Watch Video",
|
"action": "Go to Page",
|
||||||
"action_label": "Learn more about Chart of Accounts",
|
"action_label": "Learn more about Chart of Accounts",
|
||||||
"callback_message": "You can continue with the onboarding after exploring this page",
|
"callback_message": "You can continue with the onboarding after exploring this page",
|
||||||
"callback_title": "Awesome Work",
|
"callback_title": "Explore Chart of Accounts",
|
||||||
"creation": "2020-05-13 19:58:20.928127",
|
"creation": "2020-05-13 19:58:20.928127",
|
||||||
"description": "# Chart Of Accounts\n\nERPNext sets up a simple chart of accounts for each Company you create, but you can modify it according to business and legal requirements.",
|
"description": "# Chart Of Accounts\n\nERPNext sets up a simple chart of accounts for each Company you create, but you can modify it according to business and legal requirements.",
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"is_complete": 0,
|
"is_complete": 0,
|
||||||
"is_single": 0,
|
"is_single": 0,
|
||||||
"is_skipped": 0,
|
"is_skipped": 0,
|
||||||
"modified": "2021-08-13 11:46:25.878506",
|
"modified": "2022-06-07 14:21:26.264769",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"name": "Chart of Accounts",
|
"name": "Chart of Accounts",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
"action": "Create Entry",
|
"action": "Create Entry",
|
||||||
"action_label": "Manage Sales Tax Templates",
|
"action_label": "Manage Sales Tax Templates",
|
||||||
"creation": "2020-05-13 19:29:43.844463",
|
"creation": "2020-05-13 19:29:43.844463",
|
||||||
"description": "# Setting up Taxes\n\nERPNext lets you configure your taxes so that they are automatically applied in your buying and selling transactions. You can configure them globally or even on Items. ERPNext taxes are pre-configured for most regions.\n",
|
"description": "# Setting up Taxes\n\nERPNext lets you configure your taxes so that they are automatically applied in your buying and selling transactions. You can configure them globally or even on Items. ERPNext taxes are pre-configured for most regions.\n\n[Checkout pre-configured taxes](/app/sales-taxes-and-charges-template)\n",
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Onboarding Step",
|
"doctype": "Onboarding Step",
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"is_complete": 0,
|
"is_complete": 0,
|
||||||
"is_single": 0,
|
"is_single": 0,
|
||||||
"is_skipped": 0,
|
"is_skipped": 0,
|
||||||
"modified": "2021-08-13 11:48:37.238610",
|
"modified": "2022-06-07 14:27:15.906286",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"name": "Setup Taxes",
|
"name": "Setup Taxes",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ def set_address_details(
|
|||||||
else:
|
else:
|
||||||
party_details.update(get_company_address(company))
|
party_details.update(get_company_address(company))
|
||||||
|
|
||||||
if doctype and doctype in ["Delivery Note", "Sales Invoice", "Sales Order"]:
|
if doctype and doctype in ["Delivery Note", "Sales Invoice", "Sales Order", "Quotation"]:
|
||||||
if party_details.company_address:
|
if party_details.company_address:
|
||||||
party_details.update(
|
party_details.update(
|
||||||
get_fetch_values(doctype, "company_address", party_details.company_address)
|
get_fetch_values(doctype, "company_address", party_details.company_address)
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
|
|
||||||
{% if(filters.show_future_payments) { %}
|
{% if(filters.show_future_payments) { %}
|
||||||
{% var balance_row = data.slice(-1).pop();
|
{% var balance_row = data.slice(-1).pop();
|
||||||
var start = filters.based_on_payment_terms ? 13 : 11;
|
var start = report.columns.findIndex((elem) => (elem.fieldname == 'age'));
|
||||||
var range1 = report.columns[start].label;
|
var range1 = report.columns[start].label;
|
||||||
var range2 = report.columns[start+1].label;
|
var range2 = report.columns[start+1].label;
|
||||||
var range3 = report.columns[start+2].label;
|
var range3 = report.columns[start+2].label;
|
||||||
|
|||||||
@@ -172,11 +172,6 @@ frappe.query_reports["Accounts Receivable"] = {
|
|||||||
"label": __("Show Sales Person"),
|
"label": __("Show Sales Person"),
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "show_remarks",
|
|
||||||
"label": __("Show Remarks"),
|
|
||||||
"fieldtype": "Check",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "tax_id",
|
"fieldname": "tax_id",
|
||||||
"label": __("Tax Id"),
|
"label": __("Tax Id"),
|
||||||
|
|||||||
@@ -5,7 +5,9 @@
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, scrub
|
from frappe import _, qb, scrub
|
||||||
|
from frappe.query_builder import Criterion
|
||||||
|
from frappe.query_builder.functions import Date
|
||||||
from frappe.utils import cint, cstr, flt, getdate, nowdate
|
from frappe.utils import cint, cstr, flt, getdate, nowdate
|
||||||
|
|
||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||||
@@ -41,6 +43,8 @@ def execute(filters=None):
|
|||||||
class ReceivablePayableReport(object):
|
class ReceivablePayableReport(object):
|
||||||
def __init__(self, filters=None):
|
def __init__(self, filters=None):
|
||||||
self.filters = frappe._dict(filters or {})
|
self.filters = frappe._dict(filters or {})
|
||||||
|
self.qb_selection_filter = []
|
||||||
|
self.ple = qb.DocType("Payment Ledger Entry")
|
||||||
self.filters.report_date = getdate(self.filters.report_date or nowdate())
|
self.filters.report_date = getdate(self.filters.report_date or nowdate())
|
||||||
self.age_as_on = (
|
self.age_as_on = (
|
||||||
getdate(nowdate())
|
getdate(nowdate())
|
||||||
@@ -78,7 +82,7 @@ class ReceivablePayableReport(object):
|
|||||||
self.skip_total_row = 1
|
self.skip_total_row = 1
|
||||||
|
|
||||||
def get_data(self):
|
def get_data(self):
|
||||||
self.get_gl_entries()
|
self.get_ple_entries()
|
||||||
self.get_sales_invoices_or_customers_based_on_sales_person()
|
self.get_sales_invoices_or_customers_based_on_sales_person()
|
||||||
self.voucher_balance = OrderedDict()
|
self.voucher_balance = OrderedDict()
|
||||||
self.init_voucher_balance() # invoiced, paid, credit_note, outstanding
|
self.init_voucher_balance() # invoiced, paid, credit_note, outstanding
|
||||||
@@ -96,25 +100,25 @@ class ReceivablePayableReport(object):
|
|||||||
self.get_return_entries()
|
self.get_return_entries()
|
||||||
|
|
||||||
self.data = []
|
self.data = []
|
||||||
for gle in self.gl_entries:
|
|
||||||
self.update_voucher_balance(gle)
|
for ple in self.ple_entries:
|
||||||
|
self.update_voucher_balance(ple)
|
||||||
|
|
||||||
self.build_data()
|
self.build_data()
|
||||||
|
|
||||||
def init_voucher_balance(self):
|
def init_voucher_balance(self):
|
||||||
# build all keys, since we want to exclude vouchers beyond the report date
|
# build all keys, since we want to exclude vouchers beyond the report date
|
||||||
for gle in self.gl_entries:
|
for ple in self.ple_entries:
|
||||||
# get the balance object for voucher_type
|
# get the balance object for voucher_type
|
||||||
key = (gle.voucher_type, gle.voucher_no, gle.party)
|
key = (ple.voucher_type, ple.voucher_no, ple.party)
|
||||||
if not key in self.voucher_balance:
|
if not key in self.voucher_balance:
|
||||||
self.voucher_balance[key] = frappe._dict(
|
self.voucher_balance[key] = frappe._dict(
|
||||||
voucher_type=gle.voucher_type,
|
voucher_type=ple.voucher_type,
|
||||||
voucher_no=gle.voucher_no,
|
voucher_no=ple.voucher_no,
|
||||||
party=gle.party,
|
party=ple.party,
|
||||||
party_account=gle.account,
|
party_account=ple.account,
|
||||||
posting_date=gle.posting_date,
|
posting_date=ple.posting_date,
|
||||||
account_currency=gle.account_currency,
|
account_currency=ple.account_currency,
|
||||||
remarks=gle.remarks if self.filters.get("show_remarks") else None,
|
|
||||||
invoiced=0.0,
|
invoiced=0.0,
|
||||||
paid=0.0,
|
paid=0.0,
|
||||||
credit_note=0.0,
|
credit_note=0.0,
|
||||||
@@ -124,23 +128,22 @@ class ReceivablePayableReport(object):
|
|||||||
credit_note_in_account_currency=0.0,
|
credit_note_in_account_currency=0.0,
|
||||||
outstanding_in_account_currency=0.0,
|
outstanding_in_account_currency=0.0,
|
||||||
)
|
)
|
||||||
self.get_invoices(gle)
|
|
||||||
|
|
||||||
if self.filters.get("group_by_party"):
|
if self.filters.get("group_by_party"):
|
||||||
self.init_subtotal_row(gle.party)
|
self.init_subtotal_row(ple.party)
|
||||||
|
|
||||||
if self.filters.get("group_by_party"):
|
if self.filters.get("group_by_party"):
|
||||||
self.init_subtotal_row("Total")
|
self.init_subtotal_row("Total")
|
||||||
|
|
||||||
def get_invoices(self, gle):
|
def get_invoices(self, ple):
|
||||||
if gle.voucher_type in ("Sales Invoice", "Purchase Invoice"):
|
if ple.voucher_type in ("Sales Invoice", "Purchase Invoice"):
|
||||||
if self.filters.get("sales_person"):
|
if self.filters.get("sales_person"):
|
||||||
if gle.voucher_no in self.sales_person_records.get(
|
if ple.voucher_no in self.sales_person_records.get(
|
||||||
"Sales Invoice", []
|
"Sales Invoice", []
|
||||||
) or gle.party in self.sales_person_records.get("Customer", []):
|
) or ple.party in self.sales_person_records.get("Customer", []):
|
||||||
self.invoices.add(gle.voucher_no)
|
self.invoices.add(ple.voucher_no)
|
||||||
else:
|
else:
|
||||||
self.invoices.add(gle.voucher_no)
|
self.invoices.add(ple.voucher_no)
|
||||||
|
|
||||||
def init_subtotal_row(self, party):
|
def init_subtotal_row(self, party):
|
||||||
if not self.total_row_map.get(party):
|
if not self.total_row_map.get(party):
|
||||||
@@ -162,39 +165,49 @@ class ReceivablePayableReport(object):
|
|||||||
"range5",
|
"range5",
|
||||||
]
|
]
|
||||||
|
|
||||||
def update_voucher_balance(self, gle):
|
def get_voucher_balance(self, ple):
|
||||||
|
if self.filters.get("sales_person"):
|
||||||
|
if not (
|
||||||
|
ple.party in self.sales_person_records.get("Customer", [])
|
||||||
|
or ple.against_voucher_no in self.sales_person_records.get("Sales Invoice", [])
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
key = (ple.against_voucher_type, ple.against_voucher_no, ple.party)
|
||||||
|
row = self.voucher_balance.get(key)
|
||||||
|
return row
|
||||||
|
|
||||||
|
def update_voucher_balance(self, ple):
|
||||||
# get the row where this balance needs to be updated
|
# get the row where this balance needs to be updated
|
||||||
# if its a payment, it will return the linked invoice or will be considered as advance
|
# if its a payment, it will return the linked invoice or will be considered as advance
|
||||||
row = self.get_voucher_balance(gle)
|
row = self.get_voucher_balance(ple)
|
||||||
if not row:
|
if not row:
|
||||||
return
|
return
|
||||||
# gle_balance will be the total "debit - credit" for receivable type reports and
|
|
||||||
# and vice-versa for payable type reports
|
|
||||||
gle_balance = self.get_gle_balance(gle)
|
|
||||||
gle_balance_in_account_currency = self.get_gle_balance_in_account_currency(gle)
|
|
||||||
|
|
||||||
if gle_balance > 0:
|
amount = ple.amount
|
||||||
if gle.voucher_type in ("Journal Entry", "Payment Entry") and gle.against_voucher:
|
amount_in_account_currency = ple.amount_in_account_currency
|
||||||
# debit against sales / purchase invoice
|
|
||||||
row.paid -= gle_balance
|
# update voucher
|
||||||
row.paid_in_account_currency -= gle_balance_in_account_currency
|
if ple.amount > 0:
|
||||||
|
if (
|
||||||
|
ple.voucher_type in ["Journal Entry", "Payment Entry"]
|
||||||
|
and ple.voucher_no != ple.against_voucher_no
|
||||||
|
):
|
||||||
|
row.paid -= amount
|
||||||
|
row.paid_in_account_currency -= amount_in_account_currency
|
||||||
else:
|
else:
|
||||||
# invoice
|
row.invoiced += amount
|
||||||
row.invoiced += gle_balance
|
row.invoiced_in_account_currency += amount_in_account_currency
|
||||||
row.invoiced_in_account_currency += gle_balance_in_account_currency
|
|
||||||
else:
|
else:
|
||||||
# payment or credit note for receivables
|
if self.is_invoice(ple):
|
||||||
if self.is_invoice(gle):
|
row.credit_note -= amount
|
||||||
# stand alone debit / credit note
|
row.credit_note_in_account_currency -= amount_in_account_currency
|
||||||
row.credit_note -= gle_balance
|
|
||||||
row.credit_note_in_account_currency -= gle_balance_in_account_currency
|
|
||||||
else:
|
else:
|
||||||
# advance / unlinked payment or other adjustment
|
row.paid -= amount
|
||||||
row.paid -= gle_balance
|
row.paid_in_account_currency -= amount_in_account_currency
|
||||||
row.paid_in_account_currency -= gle_balance_in_account_currency
|
|
||||||
|
|
||||||
if gle.cost_center:
|
if ple.cost_center:
|
||||||
row.cost_center = str(gle.cost_center)
|
row.cost_center = str(ple.cost_center)
|
||||||
|
|
||||||
def update_sub_total_row(self, row, party):
|
def update_sub_total_row(self, row, party):
|
||||||
total_row = self.total_row_map.get(party)
|
total_row = self.total_row_map.get(party)
|
||||||
@@ -210,39 +223,6 @@ class ReceivablePayableReport(object):
|
|||||||
self.data.append({})
|
self.data.append({})
|
||||||
self.update_sub_total_row(sub_total_row, "Total")
|
self.update_sub_total_row(sub_total_row, "Total")
|
||||||
|
|
||||||
def get_voucher_balance(self, gle):
|
|
||||||
if self.filters.get("sales_person"):
|
|
||||||
against_voucher = gle.against_voucher or gle.voucher_no
|
|
||||||
if not (
|
|
||||||
gle.party in self.sales_person_records.get("Customer", [])
|
|
||||||
or against_voucher in self.sales_person_records.get("Sales Invoice", [])
|
|
||||||
):
|
|
||||||
return
|
|
||||||
|
|
||||||
voucher_balance = None
|
|
||||||
if gle.against_voucher:
|
|
||||||
# find invoice
|
|
||||||
against_voucher = gle.against_voucher
|
|
||||||
|
|
||||||
# If payment is made against credit note
|
|
||||||
# and credit note is made against a Sales Invoice
|
|
||||||
# then consider the payment against original sales invoice.
|
|
||||||
if gle.against_voucher_type in ("Sales Invoice", "Purchase Invoice"):
|
|
||||||
if gle.against_voucher in self.return_entries:
|
|
||||||
return_against = self.return_entries.get(gle.against_voucher)
|
|
||||||
if return_against:
|
|
||||||
against_voucher = return_against
|
|
||||||
|
|
||||||
voucher_balance = self.voucher_balance.get(
|
|
||||||
(gle.against_voucher_type, against_voucher, gle.party)
|
|
||||||
)
|
|
||||||
|
|
||||||
if not voucher_balance:
|
|
||||||
# no invoice, this is an invoice / stand-alone payment / credit note
|
|
||||||
voucher_balance = self.voucher_balance.get((gle.voucher_type, gle.voucher_no, gle.party))
|
|
||||||
|
|
||||||
return voucher_balance
|
|
||||||
|
|
||||||
def build_data(self):
|
def build_data(self):
|
||||||
# set outstanding for all the accumulated balances
|
# set outstanding for all the accumulated balances
|
||||||
# as we can use this to filter out invoices without outstanding
|
# as we can use this to filter out invoices without outstanding
|
||||||
@@ -260,6 +240,7 @@ class ReceivablePayableReport(object):
|
|||||||
if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) and (
|
if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) and (
|
||||||
abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision
|
abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision
|
||||||
):
|
):
|
||||||
|
|
||||||
# non-zero oustanding, we must consider this row
|
# non-zero oustanding, we must consider this row
|
||||||
|
|
||||||
if self.is_invoice(row) and self.filters.based_on_payment_terms:
|
if self.is_invoice(row) and self.filters.based_on_payment_terms:
|
||||||
@@ -669,48 +650,53 @@ class ReceivablePayableReport(object):
|
|||||||
index = 4
|
index = 4
|
||||||
row["range" + str(index + 1)] = row.outstanding
|
row["range" + str(index + 1)] = row.outstanding
|
||||||
|
|
||||||
def get_gl_entries(self):
|
def get_ple_entries(self):
|
||||||
# get all the GL entries filtered by the given filters
|
# get all the GL entries filtered by the given filters
|
||||||
|
|
||||||
conditions, values = self.prepare_conditions()
|
self.prepare_conditions()
|
||||||
order_by = self.get_order_by_condition()
|
|
||||||
|
|
||||||
if self.filters.show_future_payments:
|
if self.filters.show_future_payments:
|
||||||
values.insert(2, self.filters.report_date)
|
self.qb_selection_filter.append(
|
||||||
|
(
|
||||||
date_condition = """AND (posting_date <= %s
|
self.ple.posting_date.lte(self.filters.report_date)
|
||||||
OR (against_voucher IS NULL AND DATE(creation) <= %s))"""
|
| (
|
||||||
|
(self.ple.voucher_no == self.ple.against_voucher_no)
|
||||||
|
& (Date(self.ple.creation).lte(self.filters.report_date))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
date_condition = "AND posting_date <=%s"
|
self.qb_selection_filter.append(self.ple.posting_date.lte(self.filters.report_date))
|
||||||
|
|
||||||
if self.filters.get(scrub(self.party_type)):
|
ple = qb.DocType("Payment Ledger Entry")
|
||||||
select_fields = "debit_in_account_currency as debit, credit_in_account_currency as credit"
|
query = (
|
||||||
else:
|
qb.from_(ple)
|
||||||
select_fields = "debit, credit"
|
.select(
|
||||||
|
ple.account,
|
||||||
doc_currency_fields = "debit_in_account_currency, credit_in_account_currency"
|
ple.voucher_type,
|
||||||
|
ple.voucher_no,
|
||||||
remarks = ", remarks" if self.filters.get("show_remarks") else ""
|
ple.against_voucher_type,
|
||||||
|
ple.against_voucher_no,
|
||||||
self.gl_entries = frappe.db.sql(
|
ple.party_type,
|
||||||
"""
|
ple.cost_center,
|
||||||
select
|
ple.party,
|
||||||
name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center,
|
ple.posting_date,
|
||||||
against_voucher_type, against_voucher, account_currency, {0}, {1} {remarks}
|
ple.due_date,
|
||||||
from
|
ple.account_currency.as_("currency"),
|
||||||
`tabGL Entry`
|
ple.amount,
|
||||||
where
|
ple.amount_in_account_currency,
|
||||||
docstatus < 2
|
)
|
||||||
and is_cancelled = 0
|
.where(ple.delinked == 0)
|
||||||
and party_type=%s
|
.where(Criterion.all(self.qb_selection_filter))
|
||||||
and (party is not null and party != '')
|
|
||||||
{2} {3} {4}""".format(
|
|
||||||
select_fields, doc_currency_fields, date_condition, conditions, order_by, remarks=remarks
|
|
||||||
),
|
|
||||||
values,
|
|
||||||
as_dict=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self.filters.get("group_by_party"):
|
||||||
|
query = query.orderby(self.ple.party, self.ple.posting_date)
|
||||||
|
else:
|
||||||
|
query = query.orderby(self.ple.posting_date, self.ple.party)
|
||||||
|
|
||||||
|
self.ple_entries = query.run(as_dict=True)
|
||||||
|
|
||||||
def get_sales_invoices_or_customers_based_on_sales_person(self):
|
def get_sales_invoices_or_customers_based_on_sales_person(self):
|
||||||
if self.filters.get("sales_person"):
|
if self.filters.get("sales_person"):
|
||||||
lft, rgt = frappe.db.get_value("Sales Person", self.filters.get("sales_person"), ["lft", "rgt"])
|
lft, rgt = frappe.db.get_value("Sales Person", self.filters.get("sales_person"), ["lft", "rgt"])
|
||||||
@@ -731,23 +717,21 @@ class ReceivablePayableReport(object):
|
|||||||
self.sales_person_records.setdefault(d.parenttype, set()).add(d.parent)
|
self.sales_person_records.setdefault(d.parenttype, set()).add(d.parent)
|
||||||
|
|
||||||
def prepare_conditions(self):
|
def prepare_conditions(self):
|
||||||
conditions = [""]
|
self.qb_selection_filter = []
|
||||||
values = [self.party_type, self.filters.report_date]
|
|
||||||
party_type_field = scrub(self.party_type)
|
party_type_field = scrub(self.party_type)
|
||||||
|
|
||||||
self.add_common_filters(conditions, values, party_type_field)
|
self.add_common_filters(party_type_field=party_type_field)
|
||||||
|
|
||||||
if party_type_field == "customer":
|
if party_type_field == "customer":
|
||||||
self.add_customer_filters(conditions, values)
|
self.add_customer_filters()
|
||||||
|
|
||||||
elif party_type_field == "supplier":
|
elif party_type_field == "supplier":
|
||||||
self.add_supplier_filters(conditions, values)
|
self.add_supplier_filters()
|
||||||
|
|
||||||
if self.filters.cost_center:
|
if self.filters.cost_center:
|
||||||
self.get_cost_center_conditions(conditions)
|
self.get_cost_center_conditions()
|
||||||
|
|
||||||
self.add_accounting_dimensions_filters(conditions, values)
|
self.add_accounting_dimensions_filters()
|
||||||
return " and ".join(conditions), values
|
|
||||||
|
|
||||||
def get_cost_center_conditions(self, conditions):
|
def get_cost_center_conditions(self, conditions):
|
||||||
lft, rgt = frappe.db.get_value("Cost Center", self.filters.cost_center, ["lft", "rgt"])
|
lft, rgt = frappe.db.get_value("Cost Center", self.filters.cost_center, ["lft", "rgt"])
|
||||||
@@ -755,32 +739,20 @@ class ReceivablePayableReport(object):
|
|||||||
center.name
|
center.name
|
||||||
for center in frappe.get_list("Cost Center", filters={"lft": (">=", lft), "rgt": ("<=", rgt)})
|
for center in frappe.get_list("Cost Center", filters={"lft": (">=", lft), "rgt": ("<=", rgt)})
|
||||||
]
|
]
|
||||||
|
self.qb_selection_filter.append(self.ple.cost_center.isin(cost_center_list))
|
||||||
|
|
||||||
cost_center_string = '", "'.join(cost_center_list)
|
def add_common_filters(self, party_type_field):
|
||||||
conditions.append('cost_center in ("{0}")'.format(cost_center_string))
|
|
||||||
|
|
||||||
def get_order_by_condition(self):
|
|
||||||
if self.filters.get("group_by_party"):
|
|
||||||
return "order by party, posting_date"
|
|
||||||
else:
|
|
||||||
return "order by posting_date, party"
|
|
||||||
|
|
||||||
def add_common_filters(self, conditions, values, party_type_field):
|
|
||||||
if self.filters.company:
|
if self.filters.company:
|
||||||
conditions.append("company=%s")
|
self.qb_selection_filter.append(self.ple.company == self.filters.company)
|
||||||
values.append(self.filters.company)
|
|
||||||
|
|
||||||
if self.filters.finance_book:
|
if self.filters.finance_book:
|
||||||
conditions.append("ifnull(finance_book, '') in (%s, '')")
|
self.qb_selection_filter.append(self.ple.finance_book == self.filters.finance_book)
|
||||||
values.append(self.filters.finance_book)
|
|
||||||
|
|
||||||
if self.filters.get(party_type_field):
|
if self.filters.get(party_type_field):
|
||||||
conditions.append("party=%s")
|
self.qb_selection_filter.append(self.ple.party == self.filters.get(party_type_field))
|
||||||
values.append(self.filters.get(party_type_field))
|
|
||||||
|
|
||||||
if self.filters.party_account:
|
if self.filters.party_account:
|
||||||
conditions.append("account =%s")
|
self.qb_selection_filter.append(self.ple.account == self.filters.party_account)
|
||||||
values.append(self.filters.party_account)
|
|
||||||
else:
|
else:
|
||||||
# get GL with "receivable" or "payable" account_type
|
# get GL with "receivable" or "payable" account_type
|
||||||
account_type = "Receivable" if self.party_type == "Customer" else "Payable"
|
account_type = "Receivable" if self.party_type == "Customer" else "Payable"
|
||||||
@@ -792,46 +764,68 @@ class ReceivablePayableReport(object):
|
|||||||
]
|
]
|
||||||
|
|
||||||
if accounts:
|
if accounts:
|
||||||
conditions.append("account in (%s)" % ",".join(["%s"] * len(accounts)))
|
self.qb_selection_filter.append(self.ple.account.isin(accounts))
|
||||||
values += accounts
|
|
||||||
|
def add_customer_filters(
|
||||||
|
self,
|
||||||
|
):
|
||||||
|
self.customter = qb.DocType("Customer")
|
||||||
|
|
||||||
def add_customer_filters(self, conditions, values):
|
|
||||||
if self.filters.get("customer_group"):
|
if self.filters.get("customer_group"):
|
||||||
conditions.append(self.get_hierarchical_filters("Customer Group", "customer_group"))
|
self.get_hierarchical_filters("Customer Group", "customer_group")
|
||||||
|
|
||||||
if self.filters.get("territory"):
|
if self.filters.get("territory"):
|
||||||
conditions.append(self.get_hierarchical_filters("Territory", "territory"))
|
self.get_hierarchical_filters("Territory", "territory")
|
||||||
|
|
||||||
if self.filters.get("payment_terms_template"):
|
if self.filters.get("payment_terms_template"):
|
||||||
conditions.append("party in (select name from tabCustomer where payment_terms=%s)")
|
self.qb_selection_filter.append(
|
||||||
values.append(self.filters.get("payment_terms_template"))
|
self.ple.party_isin(
|
||||||
|
qb.from_(self.customer).where(
|
||||||
|
self.customer.payment_terms == self.filters.get("payment_terms_template")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if self.filters.get("sales_partner"):
|
if self.filters.get("sales_partner"):
|
||||||
conditions.append("party in (select name from tabCustomer where default_sales_partner=%s)")
|
self.qb_selection_filter.append(
|
||||||
values.append(self.filters.get("sales_partner"))
|
self.ple.party_isin(
|
||||||
|
qb.from_(self.customer).where(
|
||||||
def add_supplier_filters(self, conditions, values):
|
self.customer.default_sales_partner == self.filters.get("payment_terms_template")
|
||||||
if self.filters.get("supplier_group"):
|
)
|
||||||
conditions.append(
|
)
|
||||||
"""party in (select name from tabSupplier
|
)
|
||||||
where supplier_group=%s)"""
|
|
||||||
|
def add_supplier_filters(self):
|
||||||
|
supplier = qb.DocType("Supplier")
|
||||||
|
if self.filters.get("supplier_group"):
|
||||||
|
self.qb_selection_filter.append(
|
||||||
|
self.ple.party.isin(
|
||||||
|
qb.from_(supplier)
|
||||||
|
.select(supplier.name)
|
||||||
|
.where(supplier.supplier_group == self.filters.get("supplier_group"))
|
||||||
|
)
|
||||||
)
|
)
|
||||||
values.append(self.filters.get("supplier_group"))
|
|
||||||
|
|
||||||
if self.filters.get("payment_terms_template"):
|
if self.filters.get("payment_terms_template"):
|
||||||
conditions.append("party in (select name from tabSupplier where payment_terms=%s)")
|
self.qb_selection_filter.append(
|
||||||
values.append(self.filters.get("payment_terms_template"))
|
self.ple.party.isin(
|
||||||
|
qb.from_(supplier)
|
||||||
|
.select(supplier.name)
|
||||||
|
.where(supplier.payment_terms == self.filters.get("supplier_group"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def get_hierarchical_filters(self, doctype, key):
|
def get_hierarchical_filters(self, doctype, key):
|
||||||
lft, rgt = frappe.db.get_value(doctype, self.filters.get(key), ["lft", "rgt"])
|
lft, rgt = frappe.db.get_value(doctype, self.filters.get(key), ["lft", "rgt"])
|
||||||
|
|
||||||
return """party in (select name from tabCustomer
|
doc = qb.DocType(doctype)
|
||||||
where exists(select name from `tab{doctype}` where lft >= {lft} and rgt <= {rgt}
|
ple = self.ple
|
||||||
and name=tabCustomer.{key}))""".format(
|
customer = self.customer
|
||||||
doctype=doctype, lft=lft, rgt=rgt, key=key
|
groups = qb.from_(doc).select(doc.name).where((doc.lft >= lft) & (doc.rgt <= rgt))
|
||||||
)
|
customers = qb.from_(customer).select(customer.name).where(customer[key].isin(groups))
|
||||||
|
self.qb_selection_filter.append(ple.isin(ple.party.isin(customers)))
|
||||||
|
|
||||||
def add_accounting_dimensions_filters(self, conditions, values):
|
def add_accounting_dimensions_filters(self):
|
||||||
accounting_dimensions = get_accounting_dimensions(as_list=False)
|
accounting_dimensions = get_accounting_dimensions(as_list=False)
|
||||||
|
|
||||||
if accounting_dimensions:
|
if accounting_dimensions:
|
||||||
@@ -841,30 +835,16 @@ class ReceivablePayableReport(object):
|
|||||||
self.filters[dimension.fieldname] = get_dimension_with_children(
|
self.filters[dimension.fieldname] = get_dimension_with_children(
|
||||||
dimension.document_type, self.filters.get(dimension.fieldname)
|
dimension.document_type, self.filters.get(dimension.fieldname)
|
||||||
)
|
)
|
||||||
conditions.append("{0} in %s".format(dimension.fieldname))
|
self.qb_selection_filter.append(
|
||||||
values.append(tuple(self.filters.get(dimension.fieldname)))
|
self.ple[dimension.fieldname].isin(self.filters[dimension.fieldname])
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.qb_selection_filter.append(
|
||||||
|
self.ple[dimension.fieldname] == self.filters[dimension.fieldname]
|
||||||
|
)
|
||||||
|
|
||||||
def get_gle_balance(self, gle):
|
def is_invoice(self, ple):
|
||||||
# get the balance of the GL (debit - credit) or reverse balance based on report type
|
if ple.voucher_type in ("Sales Invoice", "Purchase Invoice"):
|
||||||
return gle.get(self.dr_or_cr) - self.get_reverse_balance(gle)
|
|
||||||
|
|
||||||
def get_gle_balance_in_account_currency(self, gle):
|
|
||||||
# get the balance of the GL (debit - credit) or reverse balance based on report type
|
|
||||||
return gle.get(
|
|
||||||
self.dr_or_cr + "_in_account_currency"
|
|
||||||
) - self.get_reverse_balance_in_account_currency(gle)
|
|
||||||
|
|
||||||
def get_reverse_balance_in_account_currency(self, gle):
|
|
||||||
return gle.get(
|
|
||||||
"debit_in_account_currency" if self.dr_or_cr == "credit" else "credit_in_account_currency"
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_reverse_balance(self, gle):
|
|
||||||
# get "credit" balance if report type is "debit" and vice versa
|
|
||||||
return gle.get("debit" if self.dr_or_cr == "credit" else "credit")
|
|
||||||
|
|
||||||
def is_invoice(self, gle):
|
|
||||||
if gle.voucher_type in ("Sales Invoice", "Purchase Invoice"):
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def get_party_details(self, party):
|
def get_party_details(self, party):
|
||||||
@@ -926,9 +906,6 @@ class ReceivablePayableReport(object):
|
|||||||
width=180,
|
width=180,
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.filters.show_remarks:
|
|
||||||
self.add_column(label=_("Remarks"), fieldname="remarks", fieldtype="Text", width=200),
|
|
||||||
|
|
||||||
self.add_column(label="Due Date", fieldtype="Date")
|
self.add_column(label="Due Date", fieldtype="Date")
|
||||||
|
|
||||||
if self.party_type == "Supplier":
|
if self.party_type == "Supplier":
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class TestAccountsReceivable(unittest.TestCase):
|
|||||||
def test_accounts_receivable(self):
|
def test_accounts_receivable(self):
|
||||||
frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 2'")
|
frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 2'")
|
||||||
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'")
|
frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'")
|
||||||
|
frappe.db.sql("delete from `tabPayment Ledger Entry` where company='_Test Company 2'")
|
||||||
|
|
||||||
filters = {
|
filters = {
|
||||||
"company": "_Test Company 2",
|
"company": "_Test Company 2",
|
||||||
|
|||||||
@@ -50,7 +50,15 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"options": "Fiscal Year",
|
"options": "Fiscal Year",
|
||||||
"default": frappe.defaults.get_user_default("fiscal_year"),
|
"default": frappe.defaults.get_user_default("fiscal_year"),
|
||||||
"reqd": 1
|
"reqd": 1,
|
||||||
|
on_change: () => {
|
||||||
|
frappe.model.with_doc("Fiscal Year", frappe.query_report.get_filter_value('from_fiscal_year'), function(r) {
|
||||||
|
let year_start_date = frappe.model.get_value("Fiscal Year", frappe.query_report.get_filter_value('from_fiscal_year'), "year_start_date");
|
||||||
|
frappe.query_report.set_filter_value({
|
||||||
|
period_start_date: year_start_date
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"to_fiscal_year",
|
"fieldname":"to_fiscal_year",
|
||||||
@@ -58,7 +66,15 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"options": "Fiscal Year",
|
"options": "Fiscal Year",
|
||||||
"default": frappe.defaults.get_user_default("fiscal_year"),
|
"default": frappe.defaults.get_user_default("fiscal_year"),
|
||||||
"reqd": 1
|
"reqd": 1,
|
||||||
|
on_change: () => {
|
||||||
|
frappe.model.with_doc("Fiscal Year", frappe.query_report.get_filter_value('to_fiscal_year'), function(r) {
|
||||||
|
let year_end_date = frappe.model.get_value("Fiscal Year", frappe.query_report.get_filter_value('to_fiscal_year'), "year_end_date");
|
||||||
|
frappe.query_report.set_filter_value({
|
||||||
|
period_end_date: year_end_date
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"finance_book",
|
"fieldname":"finance_book",
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ frappe.query_reports["Gross Profit"] = {
|
|||||||
"fieldname":"group_by",
|
"fieldname":"group_by",
|
||||||
"label": __("Group By"),
|
"label": __("Group By"),
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"options": "Invoice\nItem Code\nItem Group\nBrand\nWarehouse\nCustomer\nCustomer Group\nTerritory\nSales Person\nProject",
|
"options": "Invoice\nItem Code\nItem Group\nBrand\nWarehouse\nCustomer\nCustomer Group\nTerritory\nSales Person\nProject\nMonthly\nPayment Term",
|
||||||
"default": "Invoice"
|
"default": "Invoice"
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, scrub
|
from frappe import _, scrub
|
||||||
from frappe.utils import cint, flt
|
from frappe.utils import cint, flt, formatdate
|
||||||
|
|
||||||
from erpnext.controllers.queries import get_match_cond
|
from erpnext.controllers.queries import get_match_cond
|
||||||
from erpnext.stock.utils import get_incoming_rate
|
from erpnext.stock.utils import get_incoming_rate
|
||||||
@@ -124,6 +124,23 @@ def execute(filters=None):
|
|||||||
"gross_profit",
|
"gross_profit",
|
||||||
"gross_profit_percent",
|
"gross_profit_percent",
|
||||||
],
|
],
|
||||||
|
"monthly": [
|
||||||
|
"monthly",
|
||||||
|
"qty",
|
||||||
|
"base_rate",
|
||||||
|
"buying_rate",
|
||||||
|
"base_amount",
|
||||||
|
"buying_amount",
|
||||||
|
"gross_profit",
|
||||||
|
"gross_profit_percent",
|
||||||
|
],
|
||||||
|
"payment_term": [
|
||||||
|
"payment_term",
|
||||||
|
"base_amount",
|
||||||
|
"buying_amount",
|
||||||
|
"gross_profit",
|
||||||
|
"gross_profit_percent",
|
||||||
|
],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -317,6 +334,19 @@ def get_columns(group_wise_columns, filters):
|
|||||||
"options": "territory",
|
"options": "territory",
|
||||||
"width": 100,
|
"width": 100,
|
||||||
},
|
},
|
||||||
|
"monthly": {
|
||||||
|
"label": _("Monthly"),
|
||||||
|
"fieldname": "monthly",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"width": 100,
|
||||||
|
},
|
||||||
|
"payment_term": {
|
||||||
|
"label": _("Payment Term"),
|
||||||
|
"fieldname": "payment_term",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Payment Term",
|
||||||
|
"width": 170,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -390,6 +420,9 @@ class GrossProfitGenerator(object):
|
|||||||
buying_amount = 0
|
buying_amount = 0
|
||||||
|
|
||||||
for row in reversed(self.si_list):
|
for row in reversed(self.si_list):
|
||||||
|
if self.filters.get("group_by") == "Monthly":
|
||||||
|
row.monthly = formatdate(row.posting_date, "MMM YYYY")
|
||||||
|
|
||||||
if self.skip_row(row):
|
if self.skip_row(row):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -445,17 +478,7 @@ class GrossProfitGenerator(object):
|
|||||||
|
|
||||||
def get_average_rate_based_on_group_by(self):
|
def get_average_rate_based_on_group_by(self):
|
||||||
for key in list(self.grouped):
|
for key in list(self.grouped):
|
||||||
if self.filters.get("group_by") != "Invoice":
|
if self.filters.get("group_by") == "Invoice":
|
||||||
for i, row in enumerate(self.grouped[key]):
|
|
||||||
if i == 0:
|
|
||||||
new_row = row
|
|
||||||
else:
|
|
||||||
new_row.qty += flt(row.qty)
|
|
||||||
new_row.buying_amount += flt(row.buying_amount, self.currency_precision)
|
|
||||||
new_row.base_amount += flt(row.base_amount, self.currency_precision)
|
|
||||||
new_row = self.set_average_rate(new_row)
|
|
||||||
self.grouped_data.append(new_row)
|
|
||||||
else:
|
|
||||||
for i, row in enumerate(self.grouped[key]):
|
for i, row in enumerate(self.grouped[key]):
|
||||||
if row.indent == 1.0:
|
if row.indent == 1.0:
|
||||||
if (
|
if (
|
||||||
@@ -469,6 +492,44 @@ class GrossProfitGenerator(object):
|
|||||||
if flt(row.qty) or row.base_amount:
|
if flt(row.qty) or row.base_amount:
|
||||||
row = self.set_average_rate(row)
|
row = self.set_average_rate(row)
|
||||||
self.grouped_data.append(row)
|
self.grouped_data.append(row)
|
||||||
|
elif self.filters.get("group_by") == "Payment Term":
|
||||||
|
for i, row in enumerate(self.grouped[key]):
|
||||||
|
invoice_portion = 0
|
||||||
|
|
||||||
|
if row.is_return:
|
||||||
|
invoice_portion = 100
|
||||||
|
elif row.invoice_portion:
|
||||||
|
invoice_portion = row.invoice_portion
|
||||||
|
else:
|
||||||
|
invoice_portion = row.payment_amount * 100 / row.base_net_amount
|
||||||
|
|
||||||
|
if i == 0:
|
||||||
|
new_row = row
|
||||||
|
self.set_average_based_on_payment_term_portion(new_row, row, invoice_portion)
|
||||||
|
else:
|
||||||
|
new_row.qty += flt(row.qty)
|
||||||
|
self.set_average_based_on_payment_term_portion(new_row, row, invoice_portion, True)
|
||||||
|
|
||||||
|
new_row = self.set_average_rate(new_row)
|
||||||
|
self.grouped_data.append(new_row)
|
||||||
|
else:
|
||||||
|
for i, row in enumerate(self.grouped[key]):
|
||||||
|
if i == 0:
|
||||||
|
new_row = row
|
||||||
|
else:
|
||||||
|
new_row.qty += flt(row.qty)
|
||||||
|
new_row.buying_amount += flt(row.buying_amount, self.currency_precision)
|
||||||
|
new_row.base_amount += flt(row.base_amount, self.currency_precision)
|
||||||
|
new_row = self.set_average_rate(new_row)
|
||||||
|
self.grouped_data.append(new_row)
|
||||||
|
|
||||||
|
def set_average_based_on_payment_term_portion(self, new_row, row, invoice_portion, aggr=False):
|
||||||
|
cols = ["base_amount", "buying_amount", "gross_profit"]
|
||||||
|
for col in cols:
|
||||||
|
if aggr:
|
||||||
|
new_row[col] += row[col] * invoice_portion / 100
|
||||||
|
else:
|
||||||
|
new_row[col] = row[col] * invoice_portion / 100
|
||||||
|
|
||||||
def is_not_invoice_row(self, row):
|
def is_not_invoice_row(self, row):
|
||||||
return (self.filters.get("group_by") == "Invoice" and row.indent != 0.0) or self.filters.get(
|
return (self.filters.get("group_by") == "Invoice" and row.indent != 0.0) or self.filters.get(
|
||||||
@@ -622,6 +683,20 @@ class GrossProfitGenerator(object):
|
|||||||
sales_person_cols = ""
|
sales_person_cols = ""
|
||||||
sales_team_table = ""
|
sales_team_table = ""
|
||||||
|
|
||||||
|
if self.filters.group_by == "Payment Term":
|
||||||
|
payment_term_cols = """,if(`tabSales Invoice`.is_return = 1,
|
||||||
|
'{0}',
|
||||||
|
coalesce(schedule.payment_term, '{1}')) as payment_term,
|
||||||
|
schedule.invoice_portion,
|
||||||
|
schedule.payment_amount """.format(
|
||||||
|
_("Sales Return"), _("No Terms")
|
||||||
|
)
|
||||||
|
payment_term_table = """ left join `tabPayment Schedule` schedule on schedule.parent = `tabSales Invoice`.name and
|
||||||
|
`tabSales Invoice`.is_return = 0 """
|
||||||
|
else:
|
||||||
|
payment_term_cols = ""
|
||||||
|
payment_term_table = ""
|
||||||
|
|
||||||
if self.filters.get("sales_invoice"):
|
if self.filters.get("sales_invoice"):
|
||||||
conditions += " and `tabSales Invoice`.name = %(sales_invoice)s"
|
conditions += " and `tabSales Invoice`.name = %(sales_invoice)s"
|
||||||
|
|
||||||
@@ -644,10 +719,12 @@ class GrossProfitGenerator(object):
|
|||||||
`tabSales Invoice Item`.name as "item_row", `tabSales Invoice`.is_return,
|
`tabSales Invoice Item`.name as "item_row", `tabSales Invoice`.is_return,
|
||||||
`tabSales Invoice Item`.cost_center
|
`tabSales Invoice Item`.cost_center
|
||||||
{sales_person_cols}
|
{sales_person_cols}
|
||||||
|
{payment_term_cols}
|
||||||
from
|
from
|
||||||
`tabSales Invoice` inner join `tabSales Invoice Item`
|
`tabSales Invoice` inner join `tabSales Invoice Item`
|
||||||
on `tabSales Invoice Item`.parent = `tabSales Invoice`.name
|
on `tabSales Invoice Item`.parent = `tabSales Invoice`.name
|
||||||
{sales_team_table}
|
{sales_team_table}
|
||||||
|
{payment_term_table}
|
||||||
where
|
where
|
||||||
`tabSales Invoice`.docstatus=1 and `tabSales Invoice`.is_opening!='Yes' {conditions} {match_cond}
|
`tabSales Invoice`.docstatus=1 and `tabSales Invoice`.is_opening!='Yes' {conditions} {match_cond}
|
||||||
order by
|
order by
|
||||||
@@ -655,6 +732,8 @@ class GrossProfitGenerator(object):
|
|||||||
conditions=conditions,
|
conditions=conditions,
|
||||||
sales_person_cols=sales_person_cols,
|
sales_person_cols=sales_person_cols,
|
||||||
sales_team_table=sales_team_table,
|
sales_team_table=sales_team_table,
|
||||||
|
payment_term_cols=payment_term_cols,
|
||||||
|
payment_term_table=payment_term_table,
|
||||||
match_cond=get_match_cond("Sales Invoice"),
|
match_cond=get_match_cond("Sales Invoice"),
|
||||||
),
|
),
|
||||||
self.filters,
|
self.filters,
|
||||||
|
|||||||
@@ -443,12 +443,6 @@ def get_grand_total(filters, doctype):
|
|||||||
] # nosec
|
] # nosec
|
||||||
|
|
||||||
|
|
||||||
def get_deducted_taxes():
|
|
||||||
return frappe.db.sql_list(
|
|
||||||
"select name from `tabPurchase Taxes and Charges` where add_deduct_tax = 'Deduct'"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_tax_accounts(
|
def get_tax_accounts(
|
||||||
item_list,
|
item_list,
|
||||||
columns,
|
columns,
|
||||||
@@ -462,6 +456,7 @@ def get_tax_accounts(
|
|||||||
tax_columns = []
|
tax_columns = []
|
||||||
invoice_item_row = {}
|
invoice_item_row = {}
|
||||||
itemised_tax = {}
|
itemised_tax = {}
|
||||||
|
add_deduct_tax = "charge_type"
|
||||||
|
|
||||||
tax_amount_precision = (
|
tax_amount_precision = (
|
||||||
get_field_precision(
|
get_field_precision(
|
||||||
@@ -477,13 +472,13 @@ def get_tax_accounts(
|
|||||||
conditions = ""
|
conditions = ""
|
||||||
if doctype == "Purchase Invoice":
|
if doctype == "Purchase Invoice":
|
||||||
conditions = " and category in ('Total', 'Valuation and Total') and base_tax_amount_after_discount_amount != 0"
|
conditions = " and category in ('Total', 'Valuation and Total') and base_tax_amount_after_discount_amount != 0"
|
||||||
|
add_deduct_tax = "add_deduct_tax"
|
||||||
|
|
||||||
deducted_tax = get_deducted_taxes()
|
|
||||||
tax_details = frappe.db.sql(
|
tax_details = frappe.db.sql(
|
||||||
"""
|
"""
|
||||||
select
|
select
|
||||||
name, parent, description, item_wise_tax_detail,
|
name, parent, description, item_wise_tax_detail,
|
||||||
charge_type, base_tax_amount_after_discount_amount
|
charge_type, {add_deduct_tax}, base_tax_amount_after_discount_amount
|
||||||
from `tab%s`
|
from `tab%s`
|
||||||
where
|
where
|
||||||
parenttype = %s and docstatus = 1
|
parenttype = %s and docstatus = 1
|
||||||
@@ -491,12 +486,22 @@ def get_tax_accounts(
|
|||||||
and parent in (%s)
|
and parent in (%s)
|
||||||
%s
|
%s
|
||||||
order by description
|
order by description
|
||||||
"""
|
""".format(
|
||||||
|
add_deduct_tax=add_deduct_tax
|
||||||
|
)
|
||||||
% (tax_doctype, "%s", ", ".join(["%s"] * len(invoice_item_row)), conditions),
|
% (tax_doctype, "%s", ", ".join(["%s"] * len(invoice_item_row)), conditions),
|
||||||
tuple([doctype] + list(invoice_item_row)),
|
tuple([doctype] + list(invoice_item_row)),
|
||||||
)
|
)
|
||||||
|
|
||||||
for name, parent, description, item_wise_tax_detail, charge_type, tax_amount in tax_details:
|
for (
|
||||||
|
name,
|
||||||
|
parent,
|
||||||
|
description,
|
||||||
|
item_wise_tax_detail,
|
||||||
|
charge_type,
|
||||||
|
add_deduct_tax,
|
||||||
|
tax_amount,
|
||||||
|
) in tax_details:
|
||||||
description = handle_html(description)
|
description = handle_html(description)
|
||||||
if description not in tax_columns and tax_amount:
|
if description not in tax_columns and tax_amount:
|
||||||
# as description is text editor earlier and markup can break the column convention in reports
|
# as description is text editor earlier and markup can break the column convention in reports
|
||||||
@@ -529,7 +534,9 @@ def get_tax_accounts(
|
|||||||
if item_tax_amount:
|
if item_tax_amount:
|
||||||
tax_value = flt(item_tax_amount, tax_amount_precision)
|
tax_value = flt(item_tax_amount, tax_amount_precision)
|
||||||
tax_value = (
|
tax_value = (
|
||||||
tax_value * -1 if (doctype == "Purchase Invoice" and name in deducted_tax) else tax_value
|
tax_value * -1
|
||||||
|
if (doctype == "Purchase Invoice" and add_deduct_tax == "Deduct")
|
||||||
|
else tax_value
|
||||||
)
|
)
|
||||||
|
|
||||||
itemised_tax.setdefault(d.name, {})[description] = frappe._dict(
|
itemised_tax.setdefault(d.name, {})[description] = frappe._dict(
|
||||||
|
|||||||
@@ -346,9 +346,13 @@ def get_columns(invoice_list, additional_table_columns):
|
|||||||
def get_conditions(filters):
|
def get_conditions(filters):
|
||||||
conditions = ""
|
conditions = ""
|
||||||
|
|
||||||
|
accounting_dimensions = get_accounting_dimensions(as_list=False) or []
|
||||||
|
accounting_dimensions_list = [d.fieldname for d in accounting_dimensions]
|
||||||
|
|
||||||
if filters.get("company"):
|
if filters.get("company"):
|
||||||
conditions += " and company=%(company)s"
|
conditions += " and company=%(company)s"
|
||||||
if filters.get("customer"):
|
|
||||||
|
if filters.get("customer") and "customer" not in accounting_dimensions_list:
|
||||||
conditions += " and customer = %(customer)s"
|
conditions += " and customer = %(customer)s"
|
||||||
|
|
||||||
if filters.get("from_date"):
|
if filters.get("from_date"):
|
||||||
@@ -359,32 +363,18 @@ def get_conditions(filters):
|
|||||||
if filters.get("owner"):
|
if filters.get("owner"):
|
||||||
conditions += " and owner = %(owner)s"
|
conditions += " and owner = %(owner)s"
|
||||||
|
|
||||||
if filters.get("mode_of_payment"):
|
def get_sales_invoice_item_field_condition(field, table="Sales Invoice Item") -> str:
|
||||||
conditions += """ and exists(select name from `tabSales Invoice Payment`
|
if not filters.get(field) or field in accounting_dimensions_list:
|
||||||
where parent=`tabSales Invoice`.name
|
return ""
|
||||||
and ifnull(`tabSales Invoice Payment`.mode_of_payment, '') = %(mode_of_payment)s)"""
|
return f""" and exists(select name from `tab{table}`
|
||||||
|
where parent=`tabSales Invoice`.name
|
||||||
|
and ifnull(`tab{table}`.{field}, '') = %({field})s)"""
|
||||||
|
|
||||||
if filters.get("cost_center"):
|
conditions += get_sales_invoice_item_field_condition("mode_of_payments", "Sales Invoice Payment")
|
||||||
conditions += """ and exists(select name from `tabSales Invoice Item`
|
conditions += get_sales_invoice_item_field_condition("cost_center")
|
||||||
where parent=`tabSales Invoice`.name
|
conditions += get_sales_invoice_item_field_condition("warehouse")
|
||||||
and ifnull(`tabSales Invoice Item`.cost_center, '') = %(cost_center)s)"""
|
conditions += get_sales_invoice_item_field_condition("brand")
|
||||||
|
conditions += get_sales_invoice_item_field_condition("item_group")
|
||||||
if filters.get("warehouse"):
|
|
||||||
conditions += """ and exists(select name from `tabSales Invoice Item`
|
|
||||||
where parent=`tabSales Invoice`.name
|
|
||||||
and ifnull(`tabSales Invoice Item`.warehouse, '') = %(warehouse)s)"""
|
|
||||||
|
|
||||||
if filters.get("brand"):
|
|
||||||
conditions += """ and exists(select name from `tabSales Invoice Item`
|
|
||||||
where parent=`tabSales Invoice`.name
|
|
||||||
and ifnull(`tabSales Invoice Item`.brand, '') = %(brand)s)"""
|
|
||||||
|
|
||||||
if filters.get("item_group"):
|
|
||||||
conditions += """ and exists(select name from `tabSales Invoice Item`
|
|
||||||
where parent=`tabSales Invoice`.name
|
|
||||||
and ifnull(`tabSales Invoice Item`.item_group, '') = %(item_group)s)"""
|
|
||||||
|
|
||||||
accounting_dimensions = get_accounting_dimensions(as_list=False)
|
|
||||||
|
|
||||||
if accounting_dimensions:
|
if accounting_dimensions:
|
||||||
common_condition = """
|
common_condition = """
|
||||||
|
|||||||
@@ -160,14 +160,12 @@ def get_rootwise_opening_balances(filters, report_type):
|
|||||||
if filters.project:
|
if filters.project:
|
||||||
additional_conditions += " and project = %(project)s"
|
additional_conditions += " and project = %(project)s"
|
||||||
|
|
||||||
if filters.finance_book:
|
if filters.get("include_default_book_entries"):
|
||||||
fb_conditions = " AND finance_book = %(finance_book)s"
|
additional_conditions += (
|
||||||
if filters.include_default_book_entries:
|
" AND (finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)"
|
||||||
fb_conditions = (
|
)
|
||||||
" AND (finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)"
|
else:
|
||||||
)
|
additional_conditions += " AND (finance_book in (%(finance_book)s, '') OR finance_book IS NULL)"
|
||||||
|
|
||||||
additional_conditions += fb_conditions
|
|
||||||
|
|
||||||
accounting_dimensions = get_accounting_dimensions(as_list=False)
|
accounting_dimensions = get_accounting_dimensions(as_list=False)
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [
|
|||||||
("Item-wise Sales Register", {}),
|
("Item-wise Sales Register", {}),
|
||||||
("Item-wise Purchase Register", {}),
|
("Item-wise Purchase Register", {}),
|
||||||
("Sales Register", {}),
|
("Sales Register", {}),
|
||||||
|
("Sales Register", {"item_group": "All Item Groups"}),
|
||||||
("Purchase Register", {}),
|
("Purchase Register", {}),
|
||||||
(
|
(
|
||||||
"Tax Detail",
|
"Tax Detail",
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ class TestUtils(unittest.TestCase):
|
|||||||
stock_entry = {"item": item, "to_warehouse": "_Test Warehouse - _TC", "qty": 1, "rate": 10}
|
stock_entry = {"item": item, "to_warehouse": "_Test Warehouse - _TC", "qty": 1, "rate": 10}
|
||||||
|
|
||||||
se1 = make_stock_entry(posting_date="2022-01-01", **stock_entry)
|
se1 = make_stock_entry(posting_date="2022-01-01", **stock_entry)
|
||||||
se2 = make_stock_entry(posting_date="2022-02-01", **stock_entry)
|
|
||||||
se3 = make_stock_entry(posting_date="2022-03-01", **stock_entry)
|
se3 = make_stock_entry(posting_date="2022-03-01", **stock_entry)
|
||||||
|
se2 = make_stock_entry(posting_date="2022-02-01", **stock_entry)
|
||||||
|
|
||||||
for doc in (se1, se2, se3):
|
for doc in (se1, se2, se3):
|
||||||
vouchers.append((doc.doctype, doc.name))
|
vouchers.append((doc.doctype, doc.name))
|
||||||
|
|||||||
@@ -2,14 +2,18 @@
|
|||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
|
||||||
|
import itertools
|
||||||
from json import loads
|
from json import loads
|
||||||
from typing import List, Tuple
|
from typing import TYPE_CHECKING, List, Optional, Tuple
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
import frappe.defaults
|
import frappe.defaults
|
||||||
from frappe import _, qb, throw
|
from frappe import _, qb, throw
|
||||||
from frappe.model.meta import get_field_precision
|
from frappe.model.meta import get_field_precision
|
||||||
|
from frappe.query_builder.utils import DocType
|
||||||
from frappe.utils import cint, cstr, flt, formatdate, get_number_format_info, getdate, now, nowdate
|
from frappe.utils import cint, cstr, flt, formatdate, get_number_format_info, getdate, now, nowdate
|
||||||
|
from pypika import Order
|
||||||
|
from pypika.terms import ExistsCriterion
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
|
|
||||||
@@ -19,6 +23,9 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import g
|
|||||||
from erpnext.stock import get_warehouse_account_map
|
from erpnext.stock import get_warehouse_account_map
|
||||||
from erpnext.stock.utils import get_stock_value_on
|
from erpnext.stock.utils import get_stock_value_on
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import RepostItemValuation
|
||||||
|
|
||||||
|
|
||||||
class FiscalYearError(frappe.ValidationError):
|
class FiscalYearError(frappe.ValidationError):
|
||||||
pass
|
pass
|
||||||
@@ -28,6 +35,9 @@ class PaymentEntryUnlinkError(frappe.ValidationError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
GL_REPOSTING_CHUNK = 100
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_fiscal_year(
|
def get_fiscal_year(
|
||||||
date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False
|
date=None, fiscal_year=None, label="Date", verbose=1, company=None, as_dict=False
|
||||||
@@ -42,37 +52,32 @@ def get_fiscal_years(
|
|||||||
|
|
||||||
if not fiscal_years:
|
if not fiscal_years:
|
||||||
# if year start date is 2012-04-01, year end date should be 2013-03-31 (hence subdate)
|
# if year start date is 2012-04-01, year end date should be 2013-03-31 (hence subdate)
|
||||||
cond = ""
|
FY = DocType("Fiscal Year")
|
||||||
if fiscal_year:
|
|
||||||
cond += " and fy.name = {0}".format(frappe.db.escape(fiscal_year))
|
|
||||||
if company:
|
|
||||||
cond += """
|
|
||||||
and (not exists (select name
|
|
||||||
from `tabFiscal Year Company` fyc
|
|
||||||
where fyc.parent = fy.name)
|
|
||||||
or exists(select company
|
|
||||||
from `tabFiscal Year Company` fyc
|
|
||||||
where fyc.parent = fy.name
|
|
||||||
and fyc.company=%(company)s)
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
|
|
||||||
fiscal_years = frappe.db.sql(
|
query = (
|
||||||
"""
|
frappe.qb.from_(FY)
|
||||||
select
|
.select(FY.name, FY.year_start_date, FY.year_end_date)
|
||||||
fy.name, fy.year_start_date, fy.year_end_date
|
.where(FY.disabled == 0)
|
||||||
from
|
|
||||||
`tabFiscal Year` fy
|
|
||||||
where
|
|
||||||
disabled = 0 {0}
|
|
||||||
order by
|
|
||||||
fy.year_start_date desc""".format(
|
|
||||||
cond
|
|
||||||
),
|
|
||||||
{"company": company},
|
|
||||||
as_dict=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if fiscal_year:
|
||||||
|
query = query.where(FY.name == fiscal_year)
|
||||||
|
|
||||||
|
if company:
|
||||||
|
FYC = DocType("Fiscal Year Company")
|
||||||
|
query = query.where(
|
||||||
|
ExistsCriterion(frappe.qb.from_(FYC).select(FYC.name).where(FYC.parent == FY.name)).negate()
|
||||||
|
| ExistsCriterion(
|
||||||
|
frappe.qb.from_(FYC)
|
||||||
|
.select(FYC.company)
|
||||||
|
.where(FYC.parent == FY.name)
|
||||||
|
.where(FYC.company == company)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
query = query.orderby(FY.year_start_date, Order.desc)
|
||||||
|
fiscal_years = query.run(as_dict=True)
|
||||||
|
|
||||||
frappe.cache().hset("fiscal_years", company, fiscal_years)
|
frappe.cache().hset("fiscal_years", company, fiscal_years)
|
||||||
|
|
||||||
if not transaction_date and not fiscal_year:
|
if not transaction_date and not fiscal_year:
|
||||||
@@ -1122,38 +1127,62 @@ def update_gl_entries_after(
|
|||||||
|
|
||||||
|
|
||||||
def repost_gle_for_stock_vouchers(
|
def repost_gle_for_stock_vouchers(
|
||||||
stock_vouchers, posting_date, company=None, warehouse_account=None
|
stock_vouchers: List[Tuple[str, str]],
|
||||||
|
posting_date: str,
|
||||||
|
company: Optional[str] = None,
|
||||||
|
warehouse_account=None,
|
||||||
|
repost_doc: Optional["RepostItemValuation"] = None,
|
||||||
):
|
):
|
||||||
|
|
||||||
|
from erpnext.accounts.general_ledger import toggle_debit_credit_if_negative
|
||||||
|
|
||||||
if not stock_vouchers:
|
if not stock_vouchers:
|
||||||
return
|
return
|
||||||
|
|
||||||
def _delete_gl_entries(voucher_type, voucher_no):
|
|
||||||
frappe.db.sql(
|
|
||||||
"""delete from `tabGL Entry`
|
|
||||||
where voucher_type=%s and voucher_no=%s""",
|
|
||||||
(voucher_type, voucher_no),
|
|
||||||
)
|
|
||||||
|
|
||||||
stock_vouchers = sort_stock_vouchers_by_posting_date(stock_vouchers)
|
|
||||||
|
|
||||||
if not warehouse_account:
|
if not warehouse_account:
|
||||||
warehouse_account = get_warehouse_account_map(company)
|
warehouse_account = get_warehouse_account_map(company)
|
||||||
|
|
||||||
|
stock_vouchers = sort_stock_vouchers_by_posting_date(stock_vouchers)
|
||||||
|
if repost_doc and repost_doc.gl_reposting_index:
|
||||||
|
# Restore progress
|
||||||
|
stock_vouchers = stock_vouchers[cint(repost_doc.gl_reposting_index) :]
|
||||||
|
|
||||||
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit")) or 2
|
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit")) or 2
|
||||||
|
|
||||||
gle = get_voucherwise_gl_entries(stock_vouchers, posting_date)
|
stock_vouchers_iterator = iter(stock_vouchers)
|
||||||
for voucher_type, voucher_no in stock_vouchers:
|
|
||||||
existing_gle = gle.get((voucher_type, voucher_no), [])
|
while stock_vouchers_chunk := list(itertools.islice(stock_vouchers_iterator, GL_REPOSTING_CHUNK)):
|
||||||
voucher_obj = frappe.get_cached_doc(voucher_type, voucher_no)
|
gle = get_voucherwise_gl_entries(stock_vouchers_chunk, posting_date)
|
||||||
expected_gle = voucher_obj.get_gl_entries(warehouse_account)
|
|
||||||
if expected_gle:
|
for voucher_type, voucher_no in stock_vouchers_chunk:
|
||||||
if not existing_gle or not compare_existing_and_expected_gle(
|
existing_gle = gle.get((voucher_type, voucher_no), [])
|
||||||
existing_gle, expected_gle, precision
|
voucher_obj = frappe.get_doc(voucher_type, voucher_no)
|
||||||
):
|
# Some transactions post credit as negative debit, this is handled while posting GLE
|
||||||
|
# but while comparing we need to make sure it's flipped so comparisons are accurate
|
||||||
|
expected_gle = toggle_debit_credit_if_negative(voucher_obj.get_gl_entries(warehouse_account))
|
||||||
|
if expected_gle:
|
||||||
|
if not existing_gle or not compare_existing_and_expected_gle(
|
||||||
|
existing_gle, expected_gle, precision
|
||||||
|
):
|
||||||
|
_delete_gl_entries(voucher_type, voucher_no)
|
||||||
|
voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True)
|
||||||
|
else:
|
||||||
_delete_gl_entries(voucher_type, voucher_no)
|
_delete_gl_entries(voucher_type, voucher_no)
|
||||||
voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True)
|
frappe.db.commit()
|
||||||
else:
|
|
||||||
_delete_gl_entries(voucher_type, voucher_no)
|
if repost_doc:
|
||||||
|
repost_doc.db_set(
|
||||||
|
"gl_reposting_index",
|
||||||
|
cint(repost_doc.gl_reposting_index) + GL_REPOSTING_CHUNK,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _delete_gl_entries(voucher_type, voucher_no):
|
||||||
|
frappe.db.sql(
|
||||||
|
"""delete from `tabGL Entry`
|
||||||
|
where voucher_type=%s and voucher_no=%s""",
|
||||||
|
(voucher_type, voucher_no),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def sort_stock_vouchers_by_posting_date(
|
def sort_stock_vouchers_by_posting_date(
|
||||||
@@ -1167,6 +1196,9 @@ def sort_stock_vouchers_by_posting_date(
|
|||||||
.select(sle.voucher_type, sle.voucher_no, sle.posting_date, sle.posting_time, sle.creation)
|
.select(sle.voucher_type, sle.voucher_no, sle.posting_date, sle.posting_time, sle.creation)
|
||||||
.where((sle.is_cancelled == 0) & (sle.voucher_no.isin(voucher_nos)))
|
.where((sle.is_cancelled == 0) & (sle.voucher_no.isin(voucher_nos)))
|
||||||
.groupby(sle.voucher_type, sle.voucher_no)
|
.groupby(sle.voucher_type, sle.voucher_no)
|
||||||
|
.orderby(sle.posting_date)
|
||||||
|
.orderby(sle.posting_time)
|
||||||
|
.orderby(sle.creation)
|
||||||
).run(as_dict=True)
|
).run(as_dict=True)
|
||||||
sorted_vouchers = [(sle.voucher_type, sle.voucher_no) for sle in sles]
|
sorted_vouchers = [(sle.voucher_type, sle.voucher_no) for sle in sles]
|
||||||
|
|
||||||
|
|||||||
@@ -504,18 +504,6 @@
|
|||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
"type": "Link"
|
"type": "Link"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"dependencies": "GL Entry",
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 1,
|
|
||||||
"label": "DATEV Export",
|
|
||||||
"link_count": 0,
|
|
||||||
"link_to": "DATEV",
|
|
||||||
"link_type": "Report",
|
|
||||||
"onboard": 0,
|
|
||||||
"only_for": "Germany",
|
|
||||||
"type": "Link"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"dependencies": "GL Entry",
|
"dependencies": "GL Entry",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
@@ -1024,16 +1012,16 @@
|
|||||||
"type": "Link"
|
"type": "Link"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"dependencies": "Cost Center",
|
"dependencies": "Cost Center",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
"is_query_report": 0,
|
"is_query_report": 0,
|
||||||
"label": "Cost Center Allocation",
|
"label": "Cost Center Allocation",
|
||||||
"link_count": 0,
|
"link_count": 0,
|
||||||
"link_to": "Cost Center Allocation",
|
"link_to": "Cost Center Allocation",
|
||||||
"link_type": "DocType",
|
"link_type": "DocType",
|
||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
"type": "Link"
|
"type": "Link"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"dependencies": "Cost Center",
|
"dependencies": "Cost Center",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
@@ -1235,13 +1223,14 @@
|
|||||||
"type": "Link"
|
"type": "Link"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2022-01-13 17:25:09.835345",
|
"modified": "2022-06-10 15:49:42.990860",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Accounting",
|
"name": "Accounting",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"parent_page": "",
|
"parent_page": "",
|
||||||
"public": 1,
|
"public": 1,
|
||||||
|
"quick_lists": [],
|
||||||
"restrict_to_domain": "",
|
"restrict_to_domain": "",
|
||||||
"roles": [],
|
"roles": [],
|
||||||
"sequence_id": 2.0,
|
"sequence_id": 2.0,
|
||||||
|
|||||||
@@ -43,8 +43,6 @@ frappe.ui.form.on("Purchase Order", {
|
|||||||
erpnext.queries.setup_queries(frm, "Warehouse", function() {
|
erpnext.queries.setup_queries(frm, "Warehouse", function() {
|
||||||
return erpnext.queries.warehouse(frm.doc);
|
return erpnext.queries.warehouse(frm.doc);
|
||||||
});
|
});
|
||||||
|
|
||||||
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
apply_tds: function(frm) {
|
apply_tds: function(frm) {
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ def get_supplier_contacts(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
"""select `tabContact`.name from `tabContact`, `tabDynamic Link`
|
"""select `tabContact`.name from `tabContact`, `tabDynamic Link`
|
||||||
where `tabDynamic Link`.link_doctype = 'Supplier' and (`tabDynamic Link`.link_name=%(name)s
|
where `tabDynamic Link`.link_doctype = 'Supplier' and (`tabDynamic Link`.link_name=%(name)s
|
||||||
and `tabDynamic Link`.link_name like %(txt)s) and `tabContact`.name = `tabDynamic Link`.parent
|
and `tabDynamic Link`.link_name like %(txt)s) and `tabContact`.name = `tabDynamic Link`.parent
|
||||||
limit %(start)s, %(page_len)s""",
|
limit %(page_len)s offset %(start)s""",
|
||||||
{"start": start, "page_len": page_len, "txt": "%%%s%%" % txt, "name": filters.get("supplier")},
|
{"start": start, "page_len": page_len, "txt": "%%%s%%" % txt, "name": filters.get("supplier")},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -84,6 +84,9 @@ class Supplier(TransactionBase):
|
|||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def validate_internal_supplier(self):
|
def validate_internal_supplier(self):
|
||||||
|
if not self.is_internal_supplier:
|
||||||
|
self.represents_company = ""
|
||||||
|
|
||||||
internal_supplier = frappe.db.get_value(
|
internal_supplier = frappe.db.get_value(
|
||||||
"Supplier",
|
"Supplier",
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1465,8 +1465,8 @@ class AccountsController(TransactionBase):
|
|||||||
|
|
||||||
if not party_gle_currency and (party_account_currency != self.currency):
|
if not party_gle_currency and (party_account_currency != self.currency):
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Party Account {0} currency and document currency should be same").format(
|
_("Party Account {0} currency ({1}) and document currency ({2}) should be same").format(
|
||||||
frappe.bold(party_account)
|
frappe.bold(party_account), party_account_currency, self.currency
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
if(locate(%(_txt)s, employee_name), locate(%(_txt)s, employee_name), 99999),
|
if(locate(%(_txt)s, employee_name), locate(%(_txt)s, employee_name), 99999),
|
||||||
idx desc,
|
idx desc,
|
||||||
name, employee_name
|
name, employee_name
|
||||||
limit %(start)s, %(page_len)s""".format(
|
limit %(page_len)s offset %(start)s""".format(
|
||||||
**{
|
**{
|
||||||
"fields": ", ".join(fields),
|
"fields": ", ".join(fields),
|
||||||
"key": searchfield,
|
"key": searchfield,
|
||||||
@@ -65,7 +65,7 @@ def lead_query(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
if(locate(%(_txt)s, company_name), locate(%(_txt)s, company_name), 99999),
|
if(locate(%(_txt)s, company_name), locate(%(_txt)s, company_name), 99999),
|
||||||
idx desc,
|
idx desc,
|
||||||
name, lead_name
|
name, lead_name
|
||||||
limit %(start)s, %(page_len)s""".format(
|
limit %(page_len)s offset %(start)s""".format(
|
||||||
**{"fields": ", ".join(fields), "key": searchfield, "mcond": get_match_cond(doctype)}
|
**{"fields": ", ".join(fields), "key": searchfield, "mcond": get_match_cond(doctype)}
|
||||||
),
|
),
|
||||||
{"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len},
|
{"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len},
|
||||||
@@ -100,7 +100,7 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
if(locate(%(_txt)s, customer_name), locate(%(_txt)s, customer_name), 99999),
|
if(locate(%(_txt)s, customer_name), locate(%(_txt)s, customer_name), 99999),
|
||||||
idx desc,
|
idx desc,
|
||||||
name, customer_name
|
name, customer_name
|
||||||
limit %(start)s, %(page_len)s""".format(
|
limit %(page_len)s offset %(start)s""".format(
|
||||||
**{
|
**{
|
||||||
"fields": ", ".join(fields),
|
"fields": ", ".join(fields),
|
||||||
"scond": searchfields,
|
"scond": searchfields,
|
||||||
@@ -137,7 +137,7 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
if(locate(%(_txt)s, supplier_name), locate(%(_txt)s, supplier_name), 99999),
|
if(locate(%(_txt)s, supplier_name), locate(%(_txt)s, supplier_name), 99999),
|
||||||
idx desc,
|
idx desc,
|
||||||
name, supplier_name
|
name, supplier_name
|
||||||
limit %(start)s, %(page_len)s """.format(
|
limit %(page_len)s offset %(start)s""".format(
|
||||||
**{"field": ", ".join(fields), "key": searchfield, "mcond": get_match_cond(doctype)}
|
**{"field": ", ".join(fields), "key": searchfield, "mcond": get_match_cond(doctype)}
|
||||||
),
|
),
|
||||||
{"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len},
|
{"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len},
|
||||||
@@ -167,7 +167,7 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
AND `{searchfield}` LIKE %(txt)s
|
AND `{searchfield}` LIKE %(txt)s
|
||||||
{mcond}
|
{mcond}
|
||||||
ORDER BY idx DESC, name
|
ORDER BY idx DESC, name
|
||||||
LIMIT %(offset)s, %(limit)s
|
LIMIT %(limit)s offset %(offset)s
|
||||||
""".format(
|
""".format(
|
||||||
account_type_condition=account_type_condition,
|
account_type_condition=account_type_condition,
|
||||||
searchfield=searchfield,
|
searchfield=searchfield,
|
||||||
@@ -351,7 +351,7 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
|
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
|
||||||
idx desc,
|
idx desc,
|
||||||
`tabProject`.name asc
|
`tabProject`.name asc
|
||||||
limit {start}, {page_len}""".format(
|
limit {page_len} offset {start}""".format(
|
||||||
fields=", ".join(["`tabProject`.{0}".format(f) for f in fields]),
|
fields=", ".join(["`tabProject`.{0}".format(f) for f in fields]),
|
||||||
cond=cond,
|
cond=cond,
|
||||||
scond=searchfields,
|
scond=searchfields,
|
||||||
@@ -383,7 +383,7 @@ def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len,
|
|||||||
and return_against in (select name from `tabDelivery Note` where per_billed < 100)
|
and return_against in (select name from `tabDelivery Note` where per_billed < 100)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
%(mcond)s order by `tabDelivery Note`.`%(key)s` asc limit %(start)s, %(page_len)s
|
%(mcond)s order by `tabDelivery Note`.`%(key)s` asc limit %(page_len)s offset %(start)s
|
||||||
"""
|
"""
|
||||||
% {
|
% {
|
||||||
"fields": ", ".join(["`tabDelivery Note`.{0}".format(f) for f in fields]),
|
"fields": ", ".join(["`tabDelivery Note`.{0}".format(f) for f in fields]),
|
||||||
@@ -456,7 +456,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
{match_conditions}
|
{match_conditions}
|
||||||
group by batch_no {having_clause}
|
group by batch_no {having_clause}
|
||||||
order by batch.expiry_date, sle.batch_no desc
|
order by batch.expiry_date, sle.batch_no desc
|
||||||
limit %(start)s, %(page_len)s""".format(
|
limit %(page_len)s offset %(start)s""".format(
|
||||||
search_columns=search_columns,
|
search_columns=search_columns,
|
||||||
cond=cond,
|
cond=cond,
|
||||||
match_conditions=get_match_cond(doctype),
|
match_conditions=get_match_cond(doctype),
|
||||||
@@ -483,7 +483,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
{match_conditions}
|
{match_conditions}
|
||||||
|
|
||||||
order by expiry_date, name desc
|
order by expiry_date, name desc
|
||||||
limit %(start)s, %(page_len)s""".format(
|
limit %(page_len)s offset %(start)s""".format(
|
||||||
cond,
|
cond,
|
||||||
search_columns=search_columns,
|
search_columns=search_columns,
|
||||||
search_cond=search_cond,
|
search_cond=search_cond,
|
||||||
@@ -662,7 +662,7 @@ def warehouse_query(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
{fcond} {mcond}
|
{fcond} {mcond}
|
||||||
order by ifnull(`tabBin`.actual_qty, 0) desc
|
order by ifnull(`tabBin`.actual_qty, 0) desc
|
||||||
limit
|
limit
|
||||||
{start}, {page_len}
|
{page_len} offset {start}
|
||||||
""".format(
|
""".format(
|
||||||
bin_conditions=get_filters_cond(
|
bin_conditions=get_filters_cond(
|
||||||
doctype, filter_dict.get("Bin"), bin_conditions, ignore_permissions=True
|
doctype, filter_dict.get("Bin"), bin_conditions, ignore_permissions=True
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ def get_returned_qty_map_for_row(return_against, party, row_name, doctype):
|
|||||||
return data[0]
|
return data[0]
|
||||||
|
|
||||||
|
|
||||||
def make_return_doc(doctype, source_name, target_doc=None):
|
def make_return_doc(doctype: str, source_name: str, target_doc=None):
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
|
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ status_map = {
|
|||||||
["Draft", None],
|
["Draft", None],
|
||||||
["Open", "eval:self.docstatus==1"],
|
["Open", "eval:self.docstatus==1"],
|
||||||
["Lost", "eval:self.status=='Lost'"],
|
["Lost", "eval:self.status=='Lost'"],
|
||||||
["Ordered", "has_sales_order"],
|
["Partially Ordered", "is_partially_ordered"],
|
||||||
|
["Ordered", "is_fully_ordered"],
|
||||||
["Cancelled", "eval:self.docstatus==2"],
|
["Cancelled", "eval:self.docstatus==2"],
|
||||||
],
|
],
|
||||||
"Sales Order": [
|
"Sales Order": [
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ def is_search_module_loaded():
|
|||||||
out = cache.execute_command("MODULE LIST")
|
out = cache.execute_command("MODULE LIST")
|
||||||
|
|
||||||
parsed_output = " ".join(
|
parsed_output = " ".join(
|
||||||
(" ".join([s.decode() for s in o if not isinstance(s, int)]) for o in out)
|
(" ".join([frappe.as_unicode(s) for s in o if not isinstance(s, int)]) for o in out)
|
||||||
)
|
)
|
||||||
return "search" in parsed_output
|
return "search" in parsed_output
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
import frappe
|
|
||||||
from frappe.data_migration.doctype.data_migration_connector.connectors.base import BaseConnection
|
|
||||||
from github import Github
|
|
||||||
|
|
||||||
class GithubConnection(BaseConnection):
|
|
||||||
def __init__(self, connector):
|
|
||||||
self.connector = connector
|
|
||||||
|
|
||||||
try:
|
|
||||||
password = self.get_password()
|
|
||||||
except frappe.AuthenticationError:
|
|
||||||
password = None
|
|
||||||
|
|
||||||
if self.connector.username and password:
|
|
||||||
self.connection = Github(self.connector.username, self.get_password())
|
|
||||||
else:
|
|
||||||
self.connection = Github()
|
|
||||||
|
|
||||||
self.name_field = 'id'
|
|
||||||
|
|
||||||
def insert(self, doctype, doc):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def update(self, doctype, doc, migration_id):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def delete(self, doctype, migration_id):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get(self, remote_objectname, fields=None, filters=None, start=0, page_length=10):
|
|
||||||
repo = filters.get('repo')
|
|
||||||
|
|
||||||
if remote_objectname == 'Milestone':
|
|
||||||
return self.get_milestones(repo, start, page_length)
|
|
||||||
if remote_objectname == 'Issue':
|
|
||||||
return self.get_issues(repo, start, page_length)
|
|
||||||
|
|
||||||
def get_milestones(self, repo, start=0, page_length=10):
|
|
||||||
_repo = self.connection.get_repo(repo)
|
|
||||||
return list(_repo.get_milestones()[start:start+page_length])
|
|
||||||
|
|
||||||
def get_issues(self, repo, start=0, page_length=10):
|
|
||||||
_repo = self.connection.get_repo(repo)
|
|
||||||
return list(_repo.get_issues()[start:start+page_length])
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import frappe
|
|
||||||
|
|
||||||
|
|
||||||
def pre_process(issue):
|
|
||||||
|
|
||||||
project = frappe.db.get_value("Project", filters={"project_name": issue.milestone})
|
|
||||||
return {
|
|
||||||
"title": issue.title,
|
|
||||||
"body": frappe.utils.md_to_html(issue.body or ""),
|
|
||||||
"state": issue.state.title(),
|
|
||||||
"project": project or "",
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
{
|
|
||||||
"condition": "{\"repo\":\"frappe/erpnext\"}",
|
|
||||||
"creation": "2017-10-16 16:03:32.772191",
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Data Migration Mapping",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"is_child_table": 0,
|
|
||||||
"local_fieldname": "subject",
|
|
||||||
"remote_fieldname": "title"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"is_child_table": 0,
|
|
||||||
"local_fieldname": "description",
|
|
||||||
"remote_fieldname": "body"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"is_child_table": 0,
|
|
||||||
"local_fieldname": "status",
|
|
||||||
"remote_fieldname": "state"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"idx": 0,
|
|
||||||
"local_doctype": "Task",
|
|
||||||
"local_primary_key": "name",
|
|
||||||
"mapping_name": "Issue to Task",
|
|
||||||
"mapping_type": "Pull",
|
|
||||||
"migration_id_field": "github_sync_id",
|
|
||||||
"modified": "2017-10-20 11:48:54.575993",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"name": "Issue to Task",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"page_length": 10,
|
|
||||||
"remote_objectname": "Issue",
|
|
||||||
"remote_primary_key": "id"
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
def pre_process(milestone):
|
|
||||||
return {
|
|
||||||
"title": milestone.title,
|
|
||||||
"description": milestone.description,
|
|
||||||
"state": milestone.state.title(),
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
{
|
|
||||||
"condition": "{\"repo\": \"frappe/erpnext\"}",
|
|
||||||
"creation": "2017-10-13 11:16:49.664925",
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Data Migration Mapping",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"is_child_table": 0,
|
|
||||||
"local_fieldname": "project_name",
|
|
||||||
"remote_fieldname": "title"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"is_child_table": 0,
|
|
||||||
"local_fieldname": "notes",
|
|
||||||
"remote_fieldname": "description"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"is_child_table": 0,
|
|
||||||
"local_fieldname": "status",
|
|
||||||
"remote_fieldname": "state"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"idx": 0,
|
|
||||||
"local_doctype": "Project",
|
|
||||||
"local_primary_key": "project_name",
|
|
||||||
"mapping_name": "Milestone to Project",
|
|
||||||
"mapping_type": "Pull",
|
|
||||||
"migration_id_field": "github_sync_id",
|
|
||||||
"modified": "2017-10-20 11:48:54.552305",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"name": "Milestone to Project",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"page_length": 10,
|
|
||||||
"remote_objectname": "Milestone",
|
|
||||||
"remote_primary_key": "id"
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"creation": "2017-10-13 11:16:53.600026",
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Data Migration Plan",
|
|
||||||
"idx": 0,
|
|
||||||
"mappings": [
|
|
||||||
{
|
|
||||||
"enabled": 1,
|
|
||||||
"mapping": "Milestone to Project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"enabled": 1,
|
|
||||||
"mapping": "Issue to Task"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"modified": "2017-10-20 11:48:54.496123",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"module": "ERPNext Integrations",
|
|
||||||
"name": "GitHub Sync",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"plan_name": "GitHub Sync"
|
|
||||||
}
|
|
||||||
@@ -392,9 +392,12 @@ after_migrate = ["erpnext.setup.install.update_select_perm_after_install"]
|
|||||||
|
|
||||||
scheduler_events = {
|
scheduler_events = {
|
||||||
"cron": {
|
"cron": {
|
||||||
|
"0/5 * * * *": [
|
||||||
|
"erpnext.manufacturing.doctype.bom_update_log.bom_update_log.resume_bom_cost_update_jobs",
|
||||||
|
],
|
||||||
"0/30 * * * *": [
|
"0/30 * * * *": [
|
||||||
"erpnext.utilities.doctype.video.video.update_youtube_data",
|
"erpnext.utilities.doctype.video.video.update_youtube_data",
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
"all": [
|
"all": [
|
||||||
"erpnext.projects.doctype.project.project.project_status_update_reminder",
|
"erpnext.projects.doctype.project.project.project_status_update_reminder",
|
||||||
|
|||||||
@@ -827,7 +827,7 @@
|
|||||||
"idx": 24,
|
"idx": 24,
|
||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-04-22 16:21:55.811983",
|
"modified": "2022-06-10 01:29:32.952091",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "Employee",
|
"name": "Employee",
|
||||||
@@ -872,7 +872,6 @@
|
|||||||
],
|
],
|
||||||
"search_fields": "employee_name",
|
"search_fields": "employee_name",
|
||||||
"show_name_in_global_search": 1,
|
"show_name_in_global_search": 1,
|
||||||
"show_title_field_in_link": 1,
|
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ frappe.ui.form.on("Leave Application", {
|
|||||||
date: frm.doc.from_date,
|
date: frm.doc.from_date,
|
||||||
to_date: frm.doc.to_date,
|
to_date: frm.doc.to_date,
|
||||||
leave_type: frm.doc.leave_type,
|
leave_type: frm.doc.leave_type,
|
||||||
consider_all_leaves_in_the_allocation_period: true
|
consider_all_leaves_in_the_allocation_period: 1
|
||||||
},
|
},
|
||||||
callback: function (r) {
|
callback: function (r) {
|
||||||
if (!r.exc && r.message) {
|
if (!r.exc && r.message) {
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ class LeaveApplication(Document):
|
|||||||
share_doc_with_approver(self, self.leave_approver)
|
share_doc_with_approver(self, self.leave_approver)
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
if self.status == "Open":
|
if self.status in ["Open", "Cancelled"]:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Only Leave Applications with status 'Approved' and 'Rejected' can be submitted")
|
_("Only Leave Applications with status 'Approved' and 'Rejected' can be submitted")
|
||||||
)
|
)
|
||||||
@@ -757,22 +757,6 @@ def get_leave_details(employee, date):
|
|||||||
leave_allocation = {}
|
leave_allocation = {}
|
||||||
for d in allocation_records:
|
for d in allocation_records:
|
||||||
allocation = allocation_records.get(d, frappe._dict())
|
allocation = allocation_records.get(d, frappe._dict())
|
||||||
|
|
||||||
total_allocated_leaves = (
|
|
||||||
frappe.db.get_value(
|
|
||||||
"Leave Allocation",
|
|
||||||
{
|
|
||||||
"from_date": ("<=", date),
|
|
||||||
"to_date": (">=", date),
|
|
||||||
"employee": employee,
|
|
||||||
"leave_type": allocation.leave_type,
|
|
||||||
"docstatus": 1,
|
|
||||||
},
|
|
||||||
"SUM(total_leaves_allocated)",
|
|
||||||
)
|
|
||||||
or 0
|
|
||||||
)
|
|
||||||
|
|
||||||
remaining_leaves = get_leave_balance_on(
|
remaining_leaves = get_leave_balance_on(
|
||||||
employee, d, date, to_date=allocation.to_date, consider_all_leaves_in_the_allocation_period=True
|
employee, d, date, to_date=allocation.to_date, consider_all_leaves_in_the_allocation_period=True
|
||||||
)
|
)
|
||||||
@@ -782,10 +766,11 @@ def get_leave_details(employee, date):
|
|||||||
leaves_pending = get_leaves_pending_approval_for_period(
|
leaves_pending = get_leaves_pending_approval_for_period(
|
||||||
employee, d, allocation.from_date, end_date
|
employee, d, allocation.from_date, end_date
|
||||||
)
|
)
|
||||||
|
expired_leaves = allocation.total_leaves_allocated - (remaining_leaves + leaves_taken)
|
||||||
|
|
||||||
leave_allocation[d] = {
|
leave_allocation[d] = {
|
||||||
"total_leaves": total_allocated_leaves,
|
"total_leaves": allocation.total_leaves_allocated,
|
||||||
"expired_leaves": total_allocated_leaves - (remaining_leaves + leaves_taken),
|
"expired_leaves": expired_leaves if expired_leaves > 0 else 0,
|
||||||
"leaves_taken": leaves_taken,
|
"leaves_taken": leaves_taken,
|
||||||
"leaves_pending_approval": leaves_pending,
|
"leaves_pending_approval": leaves_pending,
|
||||||
"remaining_leaves": remaining_leaves,
|
"remaining_leaves": remaining_leaves,
|
||||||
@@ -830,7 +815,7 @@ def get_leave_balance_on(
|
|||||||
allocation_records = get_leave_allocation_records(employee, date, leave_type)
|
allocation_records = get_leave_allocation_records(employee, date, leave_type)
|
||||||
allocation = allocation_records.get(leave_type, frappe._dict())
|
allocation = allocation_records.get(leave_type, frappe._dict())
|
||||||
|
|
||||||
end_date = allocation.to_date if consider_all_leaves_in_the_allocation_period else date
|
end_date = allocation.to_date if cint(consider_all_leaves_in_the_allocation_period) else date
|
||||||
cf_expiry = get_allocation_expiry_for_cf_leaves(employee, leave_type, to_date, date)
|
cf_expiry = get_allocation_expiry_for_cf_leaves(employee, leave_type, to_date, date)
|
||||||
|
|
||||||
leaves_taken = get_leaves_for_period(employee, leave_type, allocation.from_date, end_date)
|
leaves_taken = get_leaves_for_period(employee, leave_type, allocation.from_date, end_date)
|
||||||
@@ -1117,7 +1102,7 @@ def add_leaves(events, start, end, filter_conditions=None):
|
|||||||
WHERE
|
WHERE
|
||||||
from_date <= %(end)s AND to_date >= %(start)s <= to_date
|
from_date <= %(end)s AND to_date >= %(start)s <= to_date
|
||||||
AND docstatus < 2
|
AND docstatus < 2
|
||||||
AND status != 'Rejected'
|
AND status in ('Approved', 'Open')
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if conditions:
|
if conditions:
|
||||||
@@ -1201,24 +1186,33 @@ def get_mandatory_approval(doctype):
|
|||||||
|
|
||||||
|
|
||||||
def get_approved_leaves_for_period(employee, leave_type, from_date, to_date):
|
def get_approved_leaves_for_period(employee, leave_type, from_date, to_date):
|
||||||
query = """
|
LeaveApplication = frappe.qb.DocType("Leave Application")
|
||||||
select employee, leave_type, from_date, to_date, total_leave_days
|
query = (
|
||||||
from `tabLeave Application`
|
frappe.qb.from_(LeaveApplication)
|
||||||
where employee=%(employee)s
|
.select(
|
||||||
and docstatus=1
|
LeaveApplication.employee,
|
||||||
and (from_date between %(from_date)s and %(to_date)s
|
LeaveApplication.leave_type,
|
||||||
or to_date between %(from_date)s and %(to_date)s
|
LeaveApplication.from_date,
|
||||||
or (from_date < %(from_date)s and to_date > %(to_date)s))
|
LeaveApplication.to_date,
|
||||||
"""
|
LeaveApplication.total_leave_days,
|
||||||
if leave_type:
|
)
|
||||||
query += "and leave_type=%(leave_type)s"
|
.where(
|
||||||
|
(LeaveApplication.employee == employee)
|
||||||
leave_applications = frappe.db.sql(
|
& (LeaveApplication.docstatus == 1)
|
||||||
query,
|
& (LeaveApplication.status == "Approved")
|
||||||
{"from_date": from_date, "to_date": to_date, "employee": employee, "leave_type": leave_type},
|
& (
|
||||||
as_dict=1,
|
(LeaveApplication.from_date.between(from_date, to_date))
|
||||||
|
| (LeaveApplication.to_date.between(from_date, to_date))
|
||||||
|
| ((LeaveApplication.from_date < from_date) & (LeaveApplication.to_date > to_date))
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if leave_type:
|
||||||
|
query = query.where(LeaveApplication.leave_type == leave_type)
|
||||||
|
|
||||||
|
leave_applications = query.run(as_dict=True)
|
||||||
|
|
||||||
leave_days = 0
|
leave_days = 0
|
||||||
for leave_app in leave_applications:
|
for leave_app in leave_applications:
|
||||||
if leave_app.from_date >= getdate(from_date) and leave_app.to_date <= getdate(to_date):
|
if leave_app.from_date >= getdate(from_date) and leave_app.to_date <= getdate(to_date):
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
frappe.listview_settings['Leave Application'] = {
|
frappe.listview_settings["Leave Application"] = {
|
||||||
add_fields: ["leave_type", "employee", "employee_name", "total_leave_days", "from_date", "to_date"],
|
add_fields: ["leave_type", "employee", "employee_name", "total_leave_days", "from_date", "to_date"],
|
||||||
has_indicator_for_draft: 1,
|
has_indicator_for_draft: 1,
|
||||||
get_indicator: function (doc) {
|
get_indicator: function (doc) {
|
||||||
if (doc.status === "Approved") {
|
let status_color = {
|
||||||
return [__("Approved"), "green", "status,=,Approved"];
|
"Approved": "green",
|
||||||
} else if (doc.status === "Rejected") {
|
"Rejected": "red",
|
||||||
return [__("Rejected"), "red", "status,=,Rejected"];
|
"Open": "orange",
|
||||||
} else {
|
"Cancelled": "red",
|
||||||
return [__("Open"), "red", "status,=,Open"];
|
"Submitted": "blue"
|
||||||
}
|
};
|
||||||
|
return [__(doc.status), status_color[doc.status], "status,=," + doc.status];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -76,7 +76,14 @@ _test_records = [
|
|||||||
|
|
||||||
class TestLeaveApplication(unittest.TestCase):
|
class TestLeaveApplication(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
for dt in ["Leave Application", "Leave Allocation", "Salary Slip", "Leave Ledger Entry"]:
|
for dt in [
|
||||||
|
"Leave Application",
|
||||||
|
"Leave Allocation",
|
||||||
|
"Salary Slip",
|
||||||
|
"Leave Ledger Entry",
|
||||||
|
"Leave Period",
|
||||||
|
"Leave Policy Assignment",
|
||||||
|
]:
|
||||||
frappe.db.delete(dt)
|
frappe.db.delete(dt)
|
||||||
|
|
||||||
frappe.set_user("Administrator")
|
frappe.set_user("Administrator")
|
||||||
@@ -702,59 +709,24 @@ class TestLeaveApplication(unittest.TestCase):
|
|||||||
self.assertEqual(details.leave_balance, 30)
|
self.assertEqual(details.leave_balance, 30)
|
||||||
|
|
||||||
def test_earned_leaves_creation(self):
|
def test_earned_leaves_creation(self):
|
||||||
|
from erpnext.hr.utils import allocate_earned_leaves
|
||||||
frappe.db.sql("""delete from `tabLeave Period`""")
|
|
||||||
frappe.db.sql("""delete from `tabLeave Policy Assignment`""")
|
|
||||||
frappe.db.sql("""delete from `tabLeave Allocation`""")
|
|
||||||
frappe.db.sql("""delete from `tabLeave Ledger Entry`""")
|
|
||||||
|
|
||||||
leave_period = get_leave_period()
|
leave_period = get_leave_period()
|
||||||
employee = get_employee()
|
employee = get_employee()
|
||||||
leave_type = "Test Earned Leave Type"
|
leave_type = "Test Earned Leave Type"
|
||||||
frappe.delete_doc_if_exists("Leave Type", "Test Earned Leave Type", force=1)
|
make_policy_assignment(employee, leave_type, leave_period)
|
||||||
frappe.get_doc(
|
|
||||||
dict(
|
|
||||||
leave_type_name=leave_type,
|
|
||||||
doctype="Leave Type",
|
|
||||||
is_earned_leave=1,
|
|
||||||
earned_leave_frequency="Monthly",
|
|
||||||
rounding=0.5,
|
|
||||||
max_leaves_allowed=6,
|
|
||||||
)
|
|
||||||
).insert()
|
|
||||||
|
|
||||||
leave_policy = frappe.get_doc(
|
for i in range(0, 14):
|
||||||
{
|
|
||||||
"doctype": "Leave Policy",
|
|
||||||
"title": "Test Leave Policy",
|
|
||||||
"leave_policy_details": [{"leave_type": leave_type, "annual_allocation": 6}],
|
|
||||||
}
|
|
||||||
).insert()
|
|
||||||
|
|
||||||
data = {
|
|
||||||
"assignment_based_on": "Leave Period",
|
|
||||||
"leave_policy": leave_policy.name,
|
|
||||||
"leave_period": leave_period.name,
|
|
||||||
}
|
|
||||||
|
|
||||||
leave_policy_assignments = create_assignment_for_multiple_employees(
|
|
||||||
[employee.name], frappe._dict(data)
|
|
||||||
)
|
|
||||||
|
|
||||||
from erpnext.hr.utils import allocate_earned_leaves
|
|
||||||
|
|
||||||
i = 0
|
|
||||||
while i < 14:
|
|
||||||
allocate_earned_leaves()
|
allocate_earned_leaves()
|
||||||
i += 1
|
|
||||||
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 6)
|
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 6)
|
||||||
|
|
||||||
# validate earned leaves creation without maximum leaves
|
# validate earned leaves creation without maximum leaves
|
||||||
frappe.db.set_value("Leave Type", leave_type, "max_leaves_allowed", 0)
|
frappe.db.set_value("Leave Type", leave_type, "max_leaves_allowed", 0)
|
||||||
i = 0
|
|
||||||
while i < 6:
|
for i in range(0, 6):
|
||||||
allocate_earned_leaves()
|
allocate_earned_leaves()
|
||||||
i += 1
|
|
||||||
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9)
|
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9)
|
||||||
|
|
||||||
# test to not consider current leave in leave balance while submitting
|
# test to not consider current leave in leave balance while submitting
|
||||||
@@ -970,6 +942,54 @@ class TestLeaveApplication(unittest.TestCase):
|
|||||||
self.assertEqual(leave_allocation["leaves_pending_approval"], 1)
|
self.assertEqual(leave_allocation["leaves_pending_approval"], 1)
|
||||||
self.assertEqual(leave_allocation["remaining_leaves"], 26)
|
self.assertEqual(leave_allocation["remaining_leaves"], 26)
|
||||||
|
|
||||||
|
@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
|
||||||
|
def test_get_earned_leave_details_for_dashboard(self):
|
||||||
|
from erpnext.hr.utils import allocate_earned_leaves
|
||||||
|
|
||||||
|
leave_period = get_leave_period()
|
||||||
|
employee = get_employee()
|
||||||
|
leave_type = "Test Earned Leave Type"
|
||||||
|
leave_policy_assignments = make_policy_assignment(employee, leave_type, leave_period)
|
||||||
|
allocation = frappe.db.get_value(
|
||||||
|
"Leave Allocation",
|
||||||
|
{"leave_policy_assignment": leave_policy_assignments[0]},
|
||||||
|
"name",
|
||||||
|
)
|
||||||
|
allocation = frappe.get_doc("Leave Allocation", allocation)
|
||||||
|
allocation.new_leaves_allocated = 2
|
||||||
|
allocation.save()
|
||||||
|
|
||||||
|
for i in range(0, 6):
|
||||||
|
allocate_earned_leaves()
|
||||||
|
|
||||||
|
first_sunday = get_first_sunday(self.holiday_list)
|
||||||
|
make_leave_application(
|
||||||
|
employee.name, add_days(first_sunday, 1), add_days(first_sunday, 1), leave_type
|
||||||
|
)
|
||||||
|
|
||||||
|
details = get_leave_details(employee.name, allocation.from_date)
|
||||||
|
leave_allocation = details["leave_allocation"][leave_type]
|
||||||
|
expected = {
|
||||||
|
"total_leaves": 2.0,
|
||||||
|
"expired_leaves": 0.0,
|
||||||
|
"leaves_taken": 1.0,
|
||||||
|
"leaves_pending_approval": 0.0,
|
||||||
|
"remaining_leaves": 1.0,
|
||||||
|
}
|
||||||
|
self.assertEqual(leave_allocation, expected)
|
||||||
|
|
||||||
|
details = get_leave_details(employee.name, getdate())
|
||||||
|
leave_allocation = details["leave_allocation"][leave_type]
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
"total_leaves": 5.0,
|
||||||
|
"expired_leaves": 0.0,
|
||||||
|
"leaves_taken": 1.0,
|
||||||
|
"leaves_pending_approval": 0.0,
|
||||||
|
"remaining_leaves": 4.0,
|
||||||
|
}
|
||||||
|
self.assertEqual(leave_allocation, expected)
|
||||||
|
|
||||||
@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
|
@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
|
||||||
def test_get_leave_allocation_records(self):
|
def test_get_leave_allocation_records(self):
|
||||||
employee = get_employee()
|
employee = get_employee()
|
||||||
@@ -1100,3 +1120,36 @@ def get_first_sunday(holiday_list, for_date=None):
|
|||||||
)[0][0]
|
)[0][0]
|
||||||
|
|
||||||
return first_sunday
|
return first_sunday
|
||||||
|
|
||||||
|
|
||||||
|
def make_policy_assignment(employee, leave_type, leave_period):
|
||||||
|
frappe.delete_doc_if_exists("Leave Type", leave_type, force=1)
|
||||||
|
frappe.get_doc(
|
||||||
|
dict(
|
||||||
|
leave_type_name=leave_type,
|
||||||
|
doctype="Leave Type",
|
||||||
|
is_earned_leave=1,
|
||||||
|
earned_leave_frequency="Monthly",
|
||||||
|
rounding=0.5,
|
||||||
|
max_leaves_allowed=6,
|
||||||
|
)
|
||||||
|
).insert()
|
||||||
|
|
||||||
|
leave_policy = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Leave Policy",
|
||||||
|
"title": "Test Leave Policy",
|
||||||
|
"leave_policy_details": [{"leave_type": leave_type, "annual_allocation": 6}],
|
||||||
|
}
|
||||||
|
).insert()
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"assignment_based_on": "Leave Period",
|
||||||
|
"leave_policy": leave_policy.name,
|
||||||
|
"leave_period": leave_period.name,
|
||||||
|
}
|
||||||
|
|
||||||
|
leave_policy_assignments = create_assignment_for_multiple_employees(
|
||||||
|
[employee.name], frappe._dict(data)
|
||||||
|
)
|
||||||
|
return leave_policy_assignments
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ def update_employee_work_history(employee, details, date=None, cancel=False):
|
|||||||
new_data = getdate(new_data)
|
new_data = getdate(new_data)
|
||||||
elif fieldtype == "Datetime" and new_data:
|
elif fieldtype == "Datetime" and new_data:
|
||||||
new_data = get_datetime(new_data)
|
new_data = get_datetime(new_data)
|
||||||
|
elif fieldtype in ["Currency", "Float"] and new_data:
|
||||||
|
new_data = flt(new_data)
|
||||||
setattr(employee, item.fieldname, new_data)
|
setattr(employee, item.fieldname, new_data)
|
||||||
if item.fieldname in ["department", "designation", "branch"]:
|
if item.fieldname in ["department", "designation", "branch"]:
|
||||||
internal_work_history[item.fieldname] = item.new
|
internal_work_history[item.fieldname] = item.new
|
||||||
|
|||||||
@@ -93,6 +93,12 @@ frappe.ui.form.on('Loan', {
|
|||||||
frm.trigger("make_loan_refund");
|
frm.trigger("make_loan_refund");
|
||||||
},__('Create'));
|
},__('Create'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (frm.doc.status == "Loan Closure Requested" && frm.doc.is_term_loan && !frm.doc.is_secured_loan) {
|
||||||
|
frm.add_custom_button(__('Close Loan'), function() {
|
||||||
|
frm.trigger("close_unsecured_term_loan");
|
||||||
|
},__('Status'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
frm.trigger("toggle_fields");
|
frm.trigger("toggle_fields");
|
||||||
},
|
},
|
||||||
@@ -174,6 +180,18 @@ frappe.ui.form.on('Loan', {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
close_unsecured_term_loan: function(frm) {
|
||||||
|
frappe.call({
|
||||||
|
args: {
|
||||||
|
"loan": frm.doc.name
|
||||||
|
},
|
||||||
|
method: "erpnext.loan_management.doctype.loan.loan.close_unsecured_term_loan",
|
||||||
|
callback: function () {
|
||||||
|
frm.refresh();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
request_loan_closure: function(frm) {
|
request_loan_closure: function(frm) {
|
||||||
frappe.confirm(__("Do you really want to close this loan"),
|
frappe.confirm(__("Do you really want to close this loan"),
|
||||||
function() {
|
function() {
|
||||||
|
|||||||
@@ -60,11 +60,11 @@ class Loan(AccountsController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def validate_cost_center(self):
|
def validate_cost_center(self):
|
||||||
if not self.cost_center and self.rate_of_interest != 0:
|
if not self.cost_center and self.rate_of_interest != 0.0:
|
||||||
self.cost_center = frappe.db.get_value("Company", self.company, "cost_center")
|
self.cost_center = frappe.db.get_value("Company", self.company, "cost_center")
|
||||||
|
|
||||||
if not self.cost_center:
|
if not self.cost_center:
|
||||||
frappe.throw(_("Cost center is mandatory for loans having rate of interest greater than 0"))
|
frappe.throw(_("Cost center is mandatory for loans having rate of interest greater than 0"))
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.link_loan_security_pledge()
|
self.link_loan_security_pledge()
|
||||||
@@ -73,7 +73,7 @@ class Loan(AccountsController):
|
|||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
self.unlink_loan_security_pledge()
|
self.unlink_loan_security_pledge()
|
||||||
self.ignore_linked_doctypes = ["GL Entry"]
|
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
|
||||||
|
|
||||||
def set_missing_fields(self):
|
def set_missing_fields(self):
|
||||||
if not self.company:
|
if not self.company:
|
||||||
@@ -342,6 +342,22 @@ def get_loan_application(loan_application):
|
|||||||
return loan.as_dict()
|
return loan.as_dict()
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def close_unsecured_term_loan(loan):
|
||||||
|
loan_details = frappe.db.get_value(
|
||||||
|
"Loan", {"name": loan}, ["status", "is_term_loan", "is_secured_loan"], as_dict=1
|
||||||
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
loan_details.status == "Loan Closure Requested"
|
||||||
|
and loan_details.is_term_loan
|
||||||
|
and not loan_details.is_secured_loan
|
||||||
|
):
|
||||||
|
frappe.db.set_value("Loan", loan, "status", "Closed")
|
||||||
|
else:
|
||||||
|
frappe.throw(_("Cannot close this loan until full repayment"))
|
||||||
|
|
||||||
|
|
||||||
def close_loan(loan, total_amount_paid):
|
def close_loan(loan, total_amount_paid):
|
||||||
frappe.db.set_value("Loan", loan, "total_amount_paid", total_amount_paid)
|
frappe.db.set_value("Loan", loan, "total_amount_paid", total_amount_paid)
|
||||||
frappe.db.set_value("Loan", loan, "status", "Closed")
|
frappe.db.set_value("Loan", loan, "status", "Closed")
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class LoanDisbursement(AccountsController):
|
|||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
self.set_status_and_amounts(cancel=1)
|
self.set_status_and_amounts(cancel=1)
|
||||||
self.make_gl_entries(cancel=1)
|
self.make_gl_entries(cancel=1)
|
||||||
self.ignore_linked_doctypes = ["GL Entry"]
|
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
|
||||||
|
|
||||||
def set_missing_values(self):
|
def set_missing_values(self):
|
||||||
if not self.disbursement_date:
|
if not self.disbursement_date:
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class LoanInterestAccrual(AccountsController):
|
|||||||
self.update_is_accrued()
|
self.update_is_accrued()
|
||||||
|
|
||||||
self.make_gl_entries(cancel=1)
|
self.make_gl_entries(cancel=1)
|
||||||
self.ignore_linked_doctypes = ["GL Entry"]
|
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
|
||||||
|
|
||||||
def update_is_accrued(self):
|
def update_is_accrued(self):
|
||||||
frappe.db.set_value("Repayment Schedule", self.repayment_schedule_name, "is_accrued", 0)
|
frappe.db.set_value("Repayment Schedule", self.repayment_schedule_name, "is_accrued", 0)
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class LoanRepayment(AccountsController):
|
|||||||
self.check_future_accruals()
|
self.check_future_accruals()
|
||||||
self.update_repayment_schedule(cancel=1)
|
self.update_repayment_schedule(cancel=1)
|
||||||
self.mark_as_unpaid()
|
self.mark_as_unpaid()
|
||||||
self.ignore_linked_doctypes = ["GL Entry"]
|
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
|
||||||
self.make_gl_entries(cancel=1)
|
self.make_gl_entries(cancel=1)
|
||||||
|
|
||||||
def set_missing_values(self, amounts):
|
def set_missing_values(self, amounts):
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class LoanWriteOff(AccountsController):
|
|||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
self.update_outstanding_amount(cancel=1)
|
self.update_outstanding_amount(cancel=1)
|
||||||
self.ignore_linked_doctypes = ["GL Entry"]
|
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
|
||||||
self.make_gl_entries(cancel=1)
|
self.make_gl_entries(cancel=1)
|
||||||
|
|
||||||
def update_outstanding_amount(self, cancel=0):
|
def update_outstanding_amount(self, cancel=0):
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ frappe.ui.form.on("BOM", {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!frm.doc.__islocal && frm.doc.docstatus<2) {
|
if (!frm.is_new() && frm.doc.docstatus<2) {
|
||||||
frm.add_custom_button(__("Update Cost"), function() {
|
frm.add_custom_button(__("Update Cost"), function() {
|
||||||
frm.events.update_cost(frm, true);
|
frm.events.update_cost(frm, true);
|
||||||
});
|
});
|
||||||
@@ -93,10 +93,12 @@ frappe.ui.form.on("BOM", {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
frm.add_custom_button(__("New Version"), function() {
|
if (!frm.is_new() && !frm.doc.docstatus == 0) {
|
||||||
let new_bom = frappe.model.copy_doc(frm.doc);
|
frm.add_custom_button(__("New Version"), function() {
|
||||||
frappe.set_route("Form", "BOM", new_bom.name);
|
let new_bom = frappe.model.copy_doc(frm.doc);
|
||||||
});
|
frappe.set_route("Form", "BOM", new_bom.name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if(frm.doc.docstatus==1) {
|
if(frm.doc.docstatus==1) {
|
||||||
frm.add_custom_button(__("Work Order"), function() {
|
frm.add_custom_button(__("Work Order"), function() {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
import re
|
import re
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from typing import List
|
from typing import Dict, List
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
@@ -189,6 +189,7 @@ class BOM(WebsiteGenerator):
|
|||||||
self.validate_transfer_against()
|
self.validate_transfer_against()
|
||||||
self.set_routing_operations()
|
self.set_routing_operations()
|
||||||
self.validate_operations()
|
self.validate_operations()
|
||||||
|
self.update_exploded_items(save=False)
|
||||||
self.calculate_cost()
|
self.calculate_cost()
|
||||||
self.update_stock_qty()
|
self.update_stock_qty()
|
||||||
self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate=False, save=False)
|
self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate=False, save=False)
|
||||||
@@ -386,40 +387,14 @@ class BOM(WebsiteGenerator):
|
|||||||
|
|
||||||
existing_bom_cost = self.total_cost
|
existing_bom_cost = self.total_cost
|
||||||
|
|
||||||
for d in self.get("items"):
|
|
||||||
if not d.item_code:
|
|
||||||
continue
|
|
||||||
|
|
||||||
rate = self.get_rm_rate(
|
|
||||||
{
|
|
||||||
"company": self.company,
|
|
||||||
"item_code": d.item_code,
|
|
||||||
"bom_no": d.bom_no,
|
|
||||||
"qty": d.qty,
|
|
||||||
"uom": d.uom,
|
|
||||||
"stock_uom": d.stock_uom,
|
|
||||||
"conversion_factor": d.conversion_factor,
|
|
||||||
"sourced_by_supplier": d.sourced_by_supplier,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if rate:
|
|
||||||
d.rate = rate
|
|
||||||
d.amount = flt(d.rate) * flt(d.qty)
|
|
||||||
d.base_rate = flt(d.rate) * flt(self.conversion_rate)
|
|
||||||
d.base_amount = flt(d.amount) * flt(self.conversion_rate)
|
|
||||||
|
|
||||||
if save:
|
|
||||||
d.db_update()
|
|
||||||
|
|
||||||
if self.docstatus == 1:
|
if self.docstatus == 1:
|
||||||
self.flags.ignore_validate_update_after_submit = True
|
self.flags.ignore_validate_update_after_submit = True
|
||||||
self.calculate_cost(update_hour_rate)
|
|
||||||
|
self.calculate_cost(save_updates=save, update_hour_rate=update_hour_rate)
|
||||||
|
|
||||||
if save:
|
if save:
|
||||||
self.db_update()
|
self.db_update()
|
||||||
|
|
||||||
self.update_exploded_items(save=save)
|
|
||||||
|
|
||||||
# update parent BOMs
|
# update parent BOMs
|
||||||
if self.total_cost != existing_bom_cost and update_parent:
|
if self.total_cost != existing_bom_cost and update_parent:
|
||||||
parent_boms = frappe.db.sql_list(
|
parent_boms = frappe.db.sql_list(
|
||||||
@@ -608,11 +583,15 @@ class BOM(WebsiteGenerator):
|
|||||||
bom_list.reverse()
|
bom_list.reverse()
|
||||||
return bom_list
|
return bom_list
|
||||||
|
|
||||||
def calculate_cost(self, update_hour_rate=False):
|
def calculate_cost(self, save_updates=False, update_hour_rate=False):
|
||||||
"""Calculate bom totals"""
|
"""Calculate bom totals"""
|
||||||
self.calculate_op_cost(update_hour_rate)
|
self.calculate_op_cost(update_hour_rate)
|
||||||
self.calculate_rm_cost()
|
self.calculate_rm_cost(save=save_updates)
|
||||||
self.calculate_sm_cost()
|
self.calculate_sm_cost(save=save_updates)
|
||||||
|
if save_updates:
|
||||||
|
# not via doc event, table is not regenerated and needs updation
|
||||||
|
self.calculate_exploded_cost()
|
||||||
|
|
||||||
self.total_cost = self.operating_cost + self.raw_material_cost - self.scrap_material_cost
|
self.total_cost = self.operating_cost + self.raw_material_cost - self.scrap_material_cost
|
||||||
self.base_total_cost = (
|
self.base_total_cost = (
|
||||||
self.base_operating_cost + self.base_raw_material_cost - self.base_scrap_material_cost
|
self.base_operating_cost + self.base_raw_material_cost - self.base_scrap_material_cost
|
||||||
@@ -654,12 +633,26 @@ class BOM(WebsiteGenerator):
|
|||||||
if update_hour_rate:
|
if update_hour_rate:
|
||||||
row.db_update()
|
row.db_update()
|
||||||
|
|
||||||
def calculate_rm_cost(self):
|
def calculate_rm_cost(self, save=False):
|
||||||
"""Fetch RM rate as per today's valuation rate and calculate totals"""
|
"""Fetch RM rate as per today's valuation rate and calculate totals"""
|
||||||
total_rm_cost = 0
|
total_rm_cost = 0
|
||||||
base_total_rm_cost = 0
|
base_total_rm_cost = 0
|
||||||
|
|
||||||
for d in self.get("items"):
|
for d in self.get("items"):
|
||||||
|
old_rate = d.rate
|
||||||
|
d.rate = self.get_rm_rate(
|
||||||
|
{
|
||||||
|
"company": self.company,
|
||||||
|
"item_code": d.item_code,
|
||||||
|
"bom_no": d.bom_no,
|
||||||
|
"qty": d.qty,
|
||||||
|
"uom": d.uom,
|
||||||
|
"stock_uom": d.stock_uom,
|
||||||
|
"conversion_factor": d.conversion_factor,
|
||||||
|
"sourced_by_supplier": d.sourced_by_supplier,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
d.base_rate = flt(d.rate) * flt(self.conversion_rate)
|
d.base_rate = flt(d.rate) * flt(self.conversion_rate)
|
||||||
d.amount = flt(d.rate, d.precision("rate")) * flt(d.qty, d.precision("qty"))
|
d.amount = flt(d.rate, d.precision("rate")) * flt(d.qty, d.precision("qty"))
|
||||||
d.base_amount = d.amount * flt(self.conversion_rate)
|
d.base_amount = d.amount * flt(self.conversion_rate)
|
||||||
@@ -669,11 +662,13 @@ class BOM(WebsiteGenerator):
|
|||||||
|
|
||||||
total_rm_cost += d.amount
|
total_rm_cost += d.amount
|
||||||
base_total_rm_cost += d.base_amount
|
base_total_rm_cost += d.base_amount
|
||||||
|
if save and (old_rate != d.rate):
|
||||||
|
d.db_update()
|
||||||
|
|
||||||
self.raw_material_cost = total_rm_cost
|
self.raw_material_cost = total_rm_cost
|
||||||
self.base_raw_material_cost = base_total_rm_cost
|
self.base_raw_material_cost = base_total_rm_cost
|
||||||
|
|
||||||
def calculate_sm_cost(self):
|
def calculate_sm_cost(self, save=False):
|
||||||
"""Fetch RM rate as per today's valuation rate and calculate totals"""
|
"""Fetch RM rate as per today's valuation rate and calculate totals"""
|
||||||
total_sm_cost = 0
|
total_sm_cost = 0
|
||||||
base_total_sm_cost = 0
|
base_total_sm_cost = 0
|
||||||
@@ -688,10 +683,45 @@ class BOM(WebsiteGenerator):
|
|||||||
)
|
)
|
||||||
total_sm_cost += d.amount
|
total_sm_cost += d.amount
|
||||||
base_total_sm_cost += d.base_amount
|
base_total_sm_cost += d.base_amount
|
||||||
|
if save:
|
||||||
|
d.db_update()
|
||||||
|
|
||||||
self.scrap_material_cost = total_sm_cost
|
self.scrap_material_cost = total_sm_cost
|
||||||
self.base_scrap_material_cost = base_total_sm_cost
|
self.base_scrap_material_cost = base_total_sm_cost
|
||||||
|
|
||||||
|
def calculate_exploded_cost(self):
|
||||||
|
"Set exploded row cost from it's parent BOM."
|
||||||
|
rm_rate_map = self.get_rm_rate_map()
|
||||||
|
|
||||||
|
for row in self.get("exploded_items"):
|
||||||
|
old_rate = flt(row.rate)
|
||||||
|
row.rate = rm_rate_map.get(row.item_code)
|
||||||
|
row.amount = flt(row.stock_qty) * flt(row.rate)
|
||||||
|
|
||||||
|
if old_rate != row.rate:
|
||||||
|
# Only db_update if changed
|
||||||
|
row.db_update()
|
||||||
|
|
||||||
|
def get_rm_rate_map(self) -> Dict[str, float]:
|
||||||
|
"Create Raw Material-Rate map for Exploded Items. Fetch rate from Items table or Subassembly BOM."
|
||||||
|
rm_rate_map = {}
|
||||||
|
|
||||||
|
for item in self.get("items"):
|
||||||
|
if item.bom_no:
|
||||||
|
# Get Item-Rate from Subassembly BOM
|
||||||
|
explosion_items = frappe.get_all(
|
||||||
|
"BOM Explosion Item",
|
||||||
|
filters={"parent": item.bom_no},
|
||||||
|
fields=["item_code", "rate"],
|
||||||
|
order_by=None, # to avoid sort index creation at db level (granular change)
|
||||||
|
)
|
||||||
|
explosion_item_rate = {item.item_code: flt(item.rate) for item in explosion_items}
|
||||||
|
rm_rate_map.update(explosion_item_rate)
|
||||||
|
else:
|
||||||
|
rm_rate_map[item.item_code] = flt(item.base_rate) / flt(item.conversion_factor or 1.0)
|
||||||
|
|
||||||
|
return rm_rate_map
|
||||||
|
|
||||||
def update_exploded_items(self, save=True):
|
def update_exploded_items(self, save=True):
|
||||||
"""Update Flat BOM, following will be correct data"""
|
"""Update Flat BOM, following will be correct data"""
|
||||||
self.get_exploded_items()
|
self.get_exploded_items()
|
||||||
@@ -902,44 +932,46 @@ def get_bom_item_rate(args, bom_doc):
|
|||||||
return flt(rate)
|
return flt(rate)
|
||||||
|
|
||||||
|
|
||||||
def get_valuation_rate(args):
|
def get_valuation_rate(data):
|
||||||
"""Get weighted average of valuation rate from all warehouses"""
|
"""
|
||||||
|
1) Get average valuation rate from all warehouses
|
||||||
|
2) If no value, get last valuation rate from SLE
|
||||||
|
3) If no value, get valuation rate from Item
|
||||||
|
"""
|
||||||
|
from frappe.query_builder.functions import Sum
|
||||||
|
|
||||||
total_qty, total_value, valuation_rate = 0.0, 0.0, 0.0
|
item_code, company = data.get("item_code"), data.get("company")
|
||||||
item_bins = frappe.db.sql(
|
valuation_rate = 0.0
|
||||||
"""
|
|
||||||
select
|
|
||||||
bin.actual_qty, bin.stock_value
|
|
||||||
from
|
|
||||||
`tabBin` bin, `tabWarehouse` warehouse
|
|
||||||
where
|
|
||||||
bin.item_code=%(item)s
|
|
||||||
and bin.warehouse = warehouse.name
|
|
||||||
and warehouse.company=%(company)s""",
|
|
||||||
{"item": args["item_code"], "company": args["company"]},
|
|
||||||
as_dict=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
for d in item_bins:
|
bin_table = frappe.qb.DocType("Bin")
|
||||||
total_qty += flt(d.actual_qty)
|
wh_table = frappe.qb.DocType("Warehouse")
|
||||||
total_value += flt(d.stock_value)
|
item_valuation = (
|
||||||
|
frappe.qb.from_(bin_table)
|
||||||
|
.join(wh_table)
|
||||||
|
.on(bin_table.warehouse == wh_table.name)
|
||||||
|
.select((Sum(bin_table.stock_value) / Sum(bin_table.actual_qty)).as_("valuation_rate"))
|
||||||
|
.where((bin_table.item_code == item_code) & (wh_table.company == company))
|
||||||
|
).run(as_dict=True)[0]
|
||||||
|
|
||||||
if total_qty:
|
valuation_rate = item_valuation.get("valuation_rate")
|
||||||
valuation_rate = total_value / total_qty
|
|
||||||
|
|
||||||
if valuation_rate <= 0:
|
if (valuation_rate is not None) and valuation_rate <= 0:
|
||||||
last_valuation_rate = frappe.db.sql(
|
# Explicit null value check. If None, Bins don't exist, neither does SLE
|
||||||
"""select valuation_rate
|
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||||
from `tabStock Ledger Entry`
|
last_val_rate = (
|
||||||
where item_code = %s and valuation_rate > 0 and is_cancelled = 0
|
frappe.qb.from_(sle)
|
||||||
order by posting_date desc, posting_time desc, creation desc limit 1""",
|
.select(sle.valuation_rate)
|
||||||
args["item_code"],
|
.where((sle.item_code == item_code) & (sle.valuation_rate > 0) & (sle.is_cancelled == 0))
|
||||||
)
|
.orderby(sle.posting_date, order=frappe.qb.desc)
|
||||||
|
.orderby(sle.posting_time, order=frappe.qb.desc)
|
||||||
|
.orderby(sle.creation, order=frappe.qb.desc)
|
||||||
|
.limit(1)
|
||||||
|
).run(as_dict=True)
|
||||||
|
|
||||||
valuation_rate = flt(last_valuation_rate[0][0]) if last_valuation_rate else 0
|
valuation_rate = flt(last_val_rate[0].get("valuation_rate")) if last_val_rate else 0
|
||||||
|
|
||||||
if not valuation_rate:
|
if not valuation_rate:
|
||||||
valuation_rate = frappe.db.get_value("Item", args["item_code"], "valuation_rate")
|
valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
|
||||||
|
|
||||||
return flt(valuation_rate)
|
return flt(valuation_rate)
|
||||||
|
|
||||||
@@ -1125,39 +1157,6 @@ def get_children(parent=None, is_root=False, **filters):
|
|||||||
return bom_items
|
return bom_items
|
||||||
|
|
||||||
|
|
||||||
def get_boms_in_bottom_up_order(bom_no=None):
|
|
||||||
def _get_parent(bom_no):
|
|
||||||
return frappe.db.sql_list(
|
|
||||||
"""
|
|
||||||
select distinct bom_item.parent from `tabBOM Item` bom_item
|
|
||||||
where bom_item.bom_no = %s and bom_item.docstatus=1 and bom_item.parenttype='BOM'
|
|
||||||
and exists(select bom.name from `tabBOM` bom where bom.name=bom_item.parent and bom.is_active=1)
|
|
||||||
""",
|
|
||||||
bom_no,
|
|
||||||
)
|
|
||||||
|
|
||||||
count = 0
|
|
||||||
bom_list = []
|
|
||||||
if bom_no:
|
|
||||||
bom_list.append(bom_no)
|
|
||||||
else:
|
|
||||||
# get all leaf BOMs
|
|
||||||
bom_list = frappe.db.sql_list(
|
|
||||||
"""select name from `tabBOM` bom
|
|
||||||
where docstatus=1 and is_active=1
|
|
||||||
and not exists(select bom_no from `tabBOM Item`
|
|
||||||
where parent=bom.name and ifnull(bom_no, '')!='')"""
|
|
||||||
)
|
|
||||||
|
|
||||||
while count < len(bom_list):
|
|
||||||
for child_bom in _get_parent(bom_list[count]):
|
|
||||||
if child_bom not in bom_list:
|
|
||||||
bom_list.append(child_bom)
|
|
||||||
count += 1
|
|
||||||
|
|
||||||
return bom_list
|
|
||||||
|
|
||||||
|
|
||||||
def add_additional_cost(stock_entry, work_order):
|
def add_additional_cost(stock_entry, work_order):
|
||||||
# Add non stock items cost in the additional cost
|
# Add non stock items cost in the additional cost
|
||||||
stock_entry.additional_costs = []
|
stock_entry.additional_costs = []
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ from frappe.utils import cstr, flt
|
|||||||
|
|
||||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||||
from erpnext.manufacturing.doctype.bom.bom import BOMRecursionError, item_query, make_variant_bom
|
from erpnext.manufacturing.doctype.bom.bom import BOMRecursionError, item_query, make_variant_bom
|
||||||
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
|
from erpnext.manufacturing.doctype.bom_update_log.test_bom_update_log import (
|
||||||
|
update_cost_in_all_boms_in_test,
|
||||||
|
)
|
||||||
from erpnext.stock.doctype.item.test_item import make_item
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||||
create_stock_reconciliation,
|
create_stock_reconciliation,
|
||||||
@@ -69,26 +71,31 @@ class TestBOM(FrappeTestCase):
|
|||||||
|
|
||||||
def test_update_bom_cost_in_all_boms(self):
|
def test_update_bom_cost_in_all_boms(self):
|
||||||
# get current rate for '_Test Item 2'
|
# get current rate for '_Test Item 2'
|
||||||
rm_rate = frappe.db.sql(
|
bom_rates = frappe.db.get_values(
|
||||||
"""select rate from `tabBOM Item`
|
"BOM Item",
|
||||||
where parent='BOM-_Test Item Home Desktop Manufactured-001'
|
{
|
||||||
and item_code='_Test Item 2' and docstatus=1 and parenttype='BOM'"""
|
"parent": "BOM-_Test Item Home Desktop Manufactured-001",
|
||||||
|
"item_code": "_Test Item 2",
|
||||||
|
"docstatus": 1,
|
||||||
|
},
|
||||||
|
fieldname=["rate", "base_rate"],
|
||||||
|
as_dict=True,
|
||||||
)
|
)
|
||||||
rm_rate = rm_rate[0][0] if rm_rate else 0
|
rm_base_rate = bom_rates[0].get("base_rate") if bom_rates else 0
|
||||||
|
|
||||||
# Reset item valuation rate
|
# Reset item valuation rate
|
||||||
reset_item_valuation_rate(item_code="_Test Item 2", qty=200, rate=rm_rate + 10)
|
reset_item_valuation_rate(item_code="_Test Item 2", qty=200, rate=rm_base_rate + 10)
|
||||||
|
|
||||||
# update cost of all BOMs based on latest valuation rate
|
# update cost of all BOMs based on latest valuation rate
|
||||||
update_cost()
|
update_cost_in_all_boms_in_test()
|
||||||
|
|
||||||
# check if new valuation rate updated in all BOMs
|
# check if new valuation rate updated in all BOMs
|
||||||
for d in frappe.db.sql(
|
for d in frappe.db.sql(
|
||||||
"""select rate from `tabBOM Item`
|
"""select base_rate from `tabBOM Item`
|
||||||
where item_code='_Test Item 2' and docstatus=1 and parenttype='BOM'""",
|
where item_code='_Test Item 2' and docstatus=1 and parenttype='BOM'""",
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
):
|
):
|
||||||
self.assertEqual(d.rate, rm_rate + 10)
|
self.assertEqual(d.base_rate, rm_base_rate + 10)
|
||||||
|
|
||||||
def test_bom_cost(self):
|
def test_bom_cost(self):
|
||||||
bom = frappe.copy_doc(test_records[2])
|
bom = frappe.copy_doc(test_records[2])
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"is_active": 1,
|
"is_active": 1,
|
||||||
"is_default": 1,
|
"is_default": 1,
|
||||||
"item": "_Test Item Home Desktop Manufactured",
|
"item": "_Test Item Home Desktop Manufactured",
|
||||||
|
"company": "_Test Company",
|
||||||
"quantity": 1.0
|
"quantity": 1.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -169,13 +169,15 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-10-08 16:21:29.386212",
|
"modified": "2022-05-27 13:42:23.305455",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "BOM Explosion Item",
|
"name": "BOM Explosion Item",
|
||||||
|
"naming_rule": "Random",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"autoname": "autoincrement",
|
||||||
|
"creation": "2022-05-31 17:34:39.825537",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"level",
|
||||||
|
"batch_no",
|
||||||
|
"boms_updated",
|
||||||
|
"status"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "level",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Level"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "batch_no",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Batch No."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "boms_updated",
|
||||||
|
"fieldtype": "Long Text",
|
||||||
|
"hidden": 1,
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "BOMs Updated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "status",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Status",
|
||||||
|
"options": "Pending\nCompleted",
|
||||||
|
"read_only": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2022-06-06 14:50:35.161062",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Manufacturing",
|
||||||
|
"name": "BOM Update Batch",
|
||||||
|
"naming_rule": "Autoincrement",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class BOMUpdateBatch(Document):
|
||||||
|
pass
|
||||||
@@ -13,6 +13,10 @@
|
|||||||
"update_type",
|
"update_type",
|
||||||
"status",
|
"status",
|
||||||
"error_log",
|
"error_log",
|
||||||
|
"progress_section",
|
||||||
|
"current_level",
|
||||||
|
"processed_boms",
|
||||||
|
"bom_batches",
|
||||||
"amended_from"
|
"amended_from"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
@@ -63,13 +67,36 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Error Log",
|
"label": "Error Log",
|
||||||
"options": "Error Log"
|
"options": "Error Log"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"depends_on": "eval: doc.update_type == \"Update Cost\"",
|
||||||
|
"fieldname": "progress_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Progress"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "processed_boms",
|
||||||
|
"fieldtype": "Long Text",
|
||||||
|
"hidden": 1,
|
||||||
|
"label": "Processed BOMs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "bom_batches",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"options": "BOM Update Batch"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "current_level",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "Current Level"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"in_create": 1,
|
"in_create": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-03-31 12:51:44.885102",
|
"modified": "2022-06-06 15:15:23.883251",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "BOM Update Log",
|
"name": "BOM Update Log",
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
from typing import Dict, List, Literal, Optional
|
import json
|
||||||
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import cstr, flt
|
from frappe.utils import cint, cstr
|
||||||
|
|
||||||
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
|
from erpnext.manufacturing.doctype.bom_update_log.bom_updation_utils import (
|
||||||
|
get_leaf_boms,
|
||||||
|
get_next_higher_level_boms,
|
||||||
|
handle_exception,
|
||||||
|
replace_bom,
|
||||||
|
set_values_in_log,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BOMMissingError(frappe.ValidationError):
|
class BOMMissingError(frappe.ValidationError):
|
||||||
@@ -20,6 +27,8 @@ class BOMUpdateLog(Document):
|
|||||||
self.validate_boms_are_specified()
|
self.validate_boms_are_specified()
|
||||||
self.validate_same_bom()
|
self.validate_same_bom()
|
||||||
self.validate_bom_items()
|
self.validate_bom_items()
|
||||||
|
else:
|
||||||
|
self.validate_bom_cost_update_in_progress()
|
||||||
|
|
||||||
self.status = "Queued"
|
self.status = "Queued"
|
||||||
|
|
||||||
@@ -42,123 +51,184 @@ class BOMUpdateLog(Document):
|
|||||||
if current_bom_item != new_bom_item:
|
if current_bom_item != new_bom_item:
|
||||||
frappe.throw(_("The selected BOMs are not for the same item"))
|
frappe.throw(_("The selected BOMs are not for the same item"))
|
||||||
|
|
||||||
def on_submit(self):
|
def validate_bom_cost_update_in_progress(self):
|
||||||
if frappe.flags.in_test:
|
"If another Cost Updation Log is still in progress, dont make new ones."
|
||||||
return
|
|
||||||
|
|
||||||
|
wip_log = frappe.get_all(
|
||||||
|
"BOM Update Log",
|
||||||
|
{"update_type": "Update Cost", "status": ["in", ["Queued", "In Progress"]]},
|
||||||
|
limit_page_length=1,
|
||||||
|
)
|
||||||
|
if wip_log:
|
||||||
|
log_link = frappe.utils.get_link_to_form("BOM Update Log", wip_log[0].name)
|
||||||
|
frappe.throw(
|
||||||
|
_("BOM Updation already in progress. Please wait until {0} is complete.").format(log_link),
|
||||||
|
title=_("Note"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_submit(self):
|
||||||
if self.update_type == "Replace BOM":
|
if self.update_type == "Replace BOM":
|
||||||
boms = {"current_bom": self.current_bom, "new_bom": self.new_bom}
|
boms = {"current_bom": self.current_bom, "new_bom": self.new_bom}
|
||||||
frappe.enqueue(
|
frappe.enqueue(
|
||||||
method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.run_bom_job",
|
method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.run_replace_bom_job",
|
||||||
doc=self,
|
doc=self,
|
||||||
boms=boms,
|
boms=boms,
|
||||||
timeout=40000,
|
timeout=40000,
|
||||||
|
now=frappe.flags.in_test,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
frappe.enqueue(
|
process_boms_cost_level_wise(self)
|
||||||
method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.run_bom_job",
|
|
||||||
doc=self,
|
|
||||||
update_type="Update Cost",
|
|
||||||
timeout=40000,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def replace_bom(boms: Dict) -> None:
|
def run_replace_bom_job(
|
||||||
"""Replace current BOM with new BOM in parent BOMs."""
|
|
||||||
current_bom = boms.get("current_bom")
|
|
||||||
new_bom = boms.get("new_bom")
|
|
||||||
|
|
||||||
unit_cost = get_new_bom_unit_cost(new_bom)
|
|
||||||
update_new_bom_in_bom_items(unit_cost, current_bom, new_bom)
|
|
||||||
|
|
||||||
frappe.cache().delete_key("bom_children")
|
|
||||||
parent_boms = get_parent_boms(new_bom)
|
|
||||||
|
|
||||||
for bom in parent_boms:
|
|
||||||
bom_obj = frappe.get_doc("BOM", bom)
|
|
||||||
# this is only used for versioning and we do not want
|
|
||||||
# to make separate db calls by using load_doc_before_save
|
|
||||||
# which proves to be expensive while doing bulk replace
|
|
||||||
bom_obj._doc_before_save = bom_obj
|
|
||||||
bom_obj.update_exploded_items()
|
|
||||||
bom_obj.calculate_cost()
|
|
||||||
bom_obj.update_parent_cost()
|
|
||||||
bom_obj.db_update()
|
|
||||||
if bom_obj.meta.get("track_changes") and not bom_obj.flags.ignore_version:
|
|
||||||
bom_obj.save_version()
|
|
||||||
|
|
||||||
|
|
||||||
def update_new_bom_in_bom_items(unit_cost: float, current_bom: str, new_bom: str) -> None:
|
|
||||||
bom_item = frappe.qb.DocType("BOM Item")
|
|
||||||
(
|
|
||||||
frappe.qb.update(bom_item)
|
|
||||||
.set(bom_item.bom_no, new_bom)
|
|
||||||
.set(bom_item.rate, unit_cost)
|
|
||||||
.set(bom_item.amount, (bom_item.stock_qty * unit_cost))
|
|
||||||
.where(
|
|
||||||
(bom_item.bom_no == current_bom) & (bom_item.docstatus < 2) & (bom_item.parenttype == "BOM")
|
|
||||||
)
|
|
||||||
).run()
|
|
||||||
|
|
||||||
|
|
||||||
def get_parent_boms(new_bom: str, bom_list: Optional[List] = None) -> List:
|
|
||||||
bom_list = bom_list or []
|
|
||||||
bom_item = frappe.qb.DocType("BOM Item")
|
|
||||||
|
|
||||||
parents = (
|
|
||||||
frappe.qb.from_(bom_item)
|
|
||||||
.select(bom_item.parent)
|
|
||||||
.where((bom_item.bom_no == new_bom) & (bom_item.docstatus < 2) & (bom_item.parenttype == "BOM"))
|
|
||||||
.run(as_dict=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
for d in parents:
|
|
||||||
if new_bom == d.parent:
|
|
||||||
frappe.throw(_("BOM recursion: {0} cannot be child of {1}").format(new_bom, d.parent))
|
|
||||||
|
|
||||||
bom_list.append(d.parent)
|
|
||||||
get_parent_boms(d.parent, bom_list)
|
|
||||||
|
|
||||||
return list(set(bom_list))
|
|
||||||
|
|
||||||
|
|
||||||
def get_new_bom_unit_cost(new_bom: str) -> float:
|
|
||||||
bom = frappe.qb.DocType("BOM")
|
|
||||||
new_bom_unitcost = (
|
|
||||||
frappe.qb.from_(bom).select(bom.total_cost / bom.quantity).where(bom.name == new_bom).run()
|
|
||||||
)
|
|
||||||
|
|
||||||
return flt(new_bom_unitcost[0][0])
|
|
||||||
|
|
||||||
|
|
||||||
def run_bom_job(
|
|
||||||
doc: "BOMUpdateLog",
|
doc: "BOMUpdateLog",
|
||||||
boms: Optional[Dict[str, str]] = None,
|
boms: Optional[Dict[str, str]] = None,
|
||||||
update_type: Literal["Replace BOM", "Update Cost"] = "Replace BOM",
|
|
||||||
) -> None:
|
) -> None:
|
||||||
try:
|
try:
|
||||||
doc.db_set("status", "In Progress")
|
doc.db_set("status", "In Progress")
|
||||||
|
|
||||||
if not frappe.flags.in_test:
|
if not frappe.flags.in_test:
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
|
|
||||||
frappe.db.auto_commit_on_many_writes = 1
|
frappe.db.auto_commit_on_many_writes = 1
|
||||||
|
|
||||||
boms = frappe._dict(boms or {})
|
boms = frappe._dict(boms or {})
|
||||||
|
replace_bom(boms, doc.name)
|
||||||
if update_type == "Replace BOM":
|
|
||||||
replace_bom(boms)
|
|
||||||
else:
|
|
||||||
update_cost()
|
|
||||||
|
|
||||||
doc.db_set("status", "Completed")
|
doc.db_set("status", "Completed")
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
frappe.db.rollback()
|
handle_exception(doc)
|
||||||
error_log = doc.log_error("BOM Update Tool Error")
|
|
||||||
|
|
||||||
doc.db_set("status", "Failed")
|
|
||||||
doc.db_set("error_log", error_log.name)
|
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
frappe.db.auto_commit_on_many_writes = 0
|
frappe.db.auto_commit_on_many_writes = 0
|
||||||
frappe.db.commit() # nosemgrep
|
|
||||||
|
if not frappe.flags.in_test:
|
||||||
|
frappe.db.commit() # nosemgrep
|
||||||
|
|
||||||
|
|
||||||
|
def process_boms_cost_level_wise(
|
||||||
|
update_doc: "BOMUpdateLog", parent_boms: List[str] = None
|
||||||
|
) -> Union[None, Tuple]:
|
||||||
|
"Queue jobs at the start of new BOM Level in 'Update Cost' Jobs."
|
||||||
|
|
||||||
|
current_boms = {}
|
||||||
|
values = {}
|
||||||
|
|
||||||
|
if update_doc.status == "Queued":
|
||||||
|
# First level yet to process. On Submit.
|
||||||
|
current_level = 0
|
||||||
|
current_boms = get_leaf_boms()
|
||||||
|
values = {
|
||||||
|
"processed_boms": json.dumps({}),
|
||||||
|
"status": "In Progress",
|
||||||
|
"current_level": current_level,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# Resume next level. via Cron Job.
|
||||||
|
if not parent_boms:
|
||||||
|
return
|
||||||
|
|
||||||
|
current_level = cint(update_doc.current_level) + 1
|
||||||
|
|
||||||
|
# Process the next level BOMs. Stage parents as current BOMs.
|
||||||
|
current_boms = parent_boms.copy()
|
||||||
|
values = {"current_level": current_level}
|
||||||
|
|
||||||
|
set_values_in_log(update_doc.name, values, commit=True)
|
||||||
|
queue_bom_cost_jobs(current_boms, update_doc, current_level)
|
||||||
|
|
||||||
|
|
||||||
|
def queue_bom_cost_jobs(
|
||||||
|
current_boms_list: List[str], update_doc: "BOMUpdateLog", current_level: int
|
||||||
|
) -> None:
|
||||||
|
"Queue batches of 20k BOMs of the same level to process parallelly"
|
||||||
|
batch_no = 0
|
||||||
|
|
||||||
|
while current_boms_list:
|
||||||
|
batch_no += 1
|
||||||
|
batch_size = 20_000
|
||||||
|
boms_to_process = current_boms_list[:batch_size] # slice out batch of 20k BOMs
|
||||||
|
|
||||||
|
# update list to exclude 20K (queued) BOMs
|
||||||
|
current_boms_list = current_boms_list[batch_size:] if len(current_boms_list) > batch_size else []
|
||||||
|
|
||||||
|
batch_row = update_doc.append(
|
||||||
|
"bom_batches", {"level": current_level, "batch_no": batch_no, "status": "Pending"}
|
||||||
|
)
|
||||||
|
batch_row.db_insert()
|
||||||
|
|
||||||
|
frappe.enqueue(
|
||||||
|
method="erpnext.manufacturing.doctype.bom_update_log.bom_updation_utils.update_cost_in_level",
|
||||||
|
doc=update_doc,
|
||||||
|
bom_list=boms_to_process,
|
||||||
|
batch_name=batch_row.name,
|
||||||
|
queue="long",
|
||||||
|
now=frappe.flags.in_test,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def resume_bom_cost_update_jobs():
|
||||||
|
"""
|
||||||
|
1. Checks for In Progress BOM Update Log.
|
||||||
|
2. Checks if this job has completed the _current level_.
|
||||||
|
3. If current level is complete, get parent BOMs and start next level.
|
||||||
|
4. If no parents, mark as Complete.
|
||||||
|
5. If current level is WIP, skip the Log.
|
||||||
|
|
||||||
|
Called every 5 minutes via Cron job.
|
||||||
|
"""
|
||||||
|
|
||||||
|
in_progress_logs = frappe.db.get_all(
|
||||||
|
"BOM Update Log",
|
||||||
|
{"update_type": "Update Cost", "status": "In Progress"},
|
||||||
|
["name", "processed_boms", "current_level"],
|
||||||
|
)
|
||||||
|
if not in_progress_logs:
|
||||||
|
return
|
||||||
|
|
||||||
|
for log in in_progress_logs:
|
||||||
|
# check if all log batches of current level are processed
|
||||||
|
bom_batches = frappe.db.get_all(
|
||||||
|
"BOM Update Batch",
|
||||||
|
{"parent": log.name, "level": log.current_level},
|
||||||
|
["name", "boms_updated", "status"],
|
||||||
|
)
|
||||||
|
incomplete_level = any(row.get("status") == "Pending" for row in bom_batches)
|
||||||
|
if not bom_batches or incomplete_level:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Prep parent BOMs & updated processed BOMs for next level
|
||||||
|
current_boms, processed_boms = get_processed_current_boms(log, bom_batches)
|
||||||
|
parent_boms = get_next_higher_level_boms(child_boms=current_boms, processed_boms=processed_boms)
|
||||||
|
|
||||||
|
# Unset processed BOMs if log is complete, it is used for next level BOMs
|
||||||
|
set_values_in_log(
|
||||||
|
log.name,
|
||||||
|
values={
|
||||||
|
"processed_boms": json.dumps([] if not parent_boms else processed_boms),
|
||||||
|
"status": "Completed" if not parent_boms else "In Progress",
|
||||||
|
},
|
||||||
|
commit=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if parent_boms: # there is a next level to process
|
||||||
|
process_boms_cost_level_wise(
|
||||||
|
update_doc=frappe.get_doc("BOM Update Log", log.name), parent_boms=parent_boms
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_processed_current_boms(
|
||||||
|
log: Dict[str, Any], bom_batches: Dict[str, Any]
|
||||||
|
) -> Tuple[List[str], Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Aggregate all BOMs from BOM Update Batch rows into 'processed_boms' field
|
||||||
|
and into current boms list.
|
||||||
|
"""
|
||||||
|
processed_boms = json.loads(log.processed_boms) if log.processed_boms else {}
|
||||||
|
current_boms = []
|
||||||
|
|
||||||
|
for row in bom_batches:
|
||||||
|
boms_updated = json.loads(row.boms_updated)
|
||||||
|
current_boms.extend(boms_updated)
|
||||||
|
boms_updated_dict = {bom: True for bom in boms_updated}
|
||||||
|
processed_boms.update(boms_updated_dict)
|
||||||
|
|
||||||
|
return current_boms, processed_boms
|
||||||
|
|||||||
@@ -0,0 +1,225 @@
|
|||||||
|
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import json
|
||||||
|
from collections import defaultdict
|
||||||
|
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import BOMUpdateLog
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
|
||||||
|
|
||||||
|
def replace_bom(boms: Dict, log_name: str) -> None:
|
||||||
|
"Replace current BOM with new BOM in parent BOMs."
|
||||||
|
|
||||||
|
current_bom = boms.get("current_bom")
|
||||||
|
new_bom = boms.get("new_bom")
|
||||||
|
|
||||||
|
unit_cost = get_bom_unit_cost(new_bom)
|
||||||
|
update_new_bom_in_bom_items(unit_cost, current_bom, new_bom)
|
||||||
|
|
||||||
|
frappe.cache().delete_key("bom_children")
|
||||||
|
parent_boms = get_ancestor_boms(new_bom)
|
||||||
|
|
||||||
|
for bom in parent_boms:
|
||||||
|
bom_obj = frappe.get_doc("BOM", bom)
|
||||||
|
# this is only used for versioning and we do not want
|
||||||
|
# to make separate db calls by using load_doc_before_save
|
||||||
|
# which proves to be expensive while doing bulk replace
|
||||||
|
bom_obj._doc_before_save = copy.deepcopy(bom_obj)
|
||||||
|
bom_obj.update_exploded_items()
|
||||||
|
bom_obj.calculate_cost()
|
||||||
|
bom_obj.update_parent_cost()
|
||||||
|
bom_obj.db_update()
|
||||||
|
bom_obj.flags.updater_reference = {
|
||||||
|
"doctype": "BOM Update Log",
|
||||||
|
"docname": log_name,
|
||||||
|
"label": _("via BOM Update Tool"),
|
||||||
|
}
|
||||||
|
bom_obj.save_version()
|
||||||
|
|
||||||
|
|
||||||
|
def update_cost_in_level(
|
||||||
|
doc: "BOMUpdateLog", bom_list: List[str], batch_name: Union[int, str]
|
||||||
|
) -> None:
|
||||||
|
"Updates Cost for BOMs within a given level. Runs via background jobs."
|
||||||
|
|
||||||
|
try:
|
||||||
|
status = frappe.db.get_value("BOM Update Log", doc.name, "status")
|
||||||
|
if status == "Failed":
|
||||||
|
return
|
||||||
|
|
||||||
|
update_cost_in_boms(bom_list=bom_list) # main updation logic
|
||||||
|
|
||||||
|
bom_batch = frappe.qb.DocType("BOM Update Batch")
|
||||||
|
(
|
||||||
|
frappe.qb.update(bom_batch)
|
||||||
|
.set(bom_batch.boms_updated, json.dumps(bom_list))
|
||||||
|
.set(bom_batch.status, "Completed")
|
||||||
|
.where(bom_batch.name == batch_name)
|
||||||
|
).run()
|
||||||
|
except Exception:
|
||||||
|
handle_exception(doc)
|
||||||
|
finally:
|
||||||
|
if not frappe.flags.in_test:
|
||||||
|
frappe.db.commit() # nosemgrep
|
||||||
|
|
||||||
|
|
||||||
|
def get_ancestor_boms(new_bom: str, bom_list: Optional[List] = None) -> List:
|
||||||
|
"Recursively get all ancestors of BOM."
|
||||||
|
|
||||||
|
bom_list = bom_list or []
|
||||||
|
bom_item = frappe.qb.DocType("BOM Item")
|
||||||
|
|
||||||
|
parents = (
|
||||||
|
frappe.qb.from_(bom_item)
|
||||||
|
.select(bom_item.parent)
|
||||||
|
.where((bom_item.bom_no == new_bom) & (bom_item.docstatus < 2) & (bom_item.parenttype == "BOM"))
|
||||||
|
.run(as_dict=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
for d in parents:
|
||||||
|
if new_bom == d.parent:
|
||||||
|
frappe.throw(_("BOM recursion: {0} cannot be child of {1}").format(new_bom, d.parent))
|
||||||
|
|
||||||
|
bom_list.append(d.parent)
|
||||||
|
get_ancestor_boms(d.parent, bom_list)
|
||||||
|
|
||||||
|
return list(set(bom_list))
|
||||||
|
|
||||||
|
|
||||||
|
def update_new_bom_in_bom_items(unit_cost: float, current_bom: str, new_bom: str) -> None:
|
||||||
|
bom_item = frappe.qb.DocType("BOM Item")
|
||||||
|
(
|
||||||
|
frappe.qb.update(bom_item)
|
||||||
|
.set(bom_item.bom_no, new_bom)
|
||||||
|
.set(bom_item.rate, unit_cost)
|
||||||
|
.set(bom_item.amount, (bom_item.stock_qty * unit_cost))
|
||||||
|
.where(
|
||||||
|
(bom_item.bom_no == current_bom) & (bom_item.docstatus < 2) & (bom_item.parenttype == "BOM")
|
||||||
|
)
|
||||||
|
).run()
|
||||||
|
|
||||||
|
|
||||||
|
def get_bom_unit_cost(bom_name: str) -> float:
|
||||||
|
bom = frappe.qb.DocType("BOM")
|
||||||
|
new_bom_unitcost = (
|
||||||
|
frappe.qb.from_(bom).select(bom.total_cost / bom.quantity).where(bom.name == bom_name).run()
|
||||||
|
)
|
||||||
|
|
||||||
|
return frappe.utils.flt(new_bom_unitcost[0][0])
|
||||||
|
|
||||||
|
|
||||||
|
def update_cost_in_boms(bom_list: List[str]) -> None:
|
||||||
|
"Updates cost in given BOMs. Returns current and total updated BOMs."
|
||||||
|
|
||||||
|
for index, bom in enumerate(bom_list):
|
||||||
|
bom_doc = frappe.get_doc("BOM", bom, for_update=True)
|
||||||
|
bom_doc.calculate_cost(save_updates=True, update_hour_rate=True)
|
||||||
|
bom_doc.db_update()
|
||||||
|
|
||||||
|
if (index % 50 == 0) and not frappe.flags.in_test:
|
||||||
|
frappe.db.commit() # nosemgrep
|
||||||
|
|
||||||
|
|
||||||
|
def get_next_higher_level_boms(
|
||||||
|
child_boms: List[str], processed_boms: Dict[str, bool]
|
||||||
|
) -> List[str]:
|
||||||
|
"Generate immediate higher level dependants with no unresolved dependencies (children)."
|
||||||
|
|
||||||
|
def _all_children_are_processed(parent_bom):
|
||||||
|
child_boms = dependency_map.get(parent_bom)
|
||||||
|
return all(processed_boms.get(bom) for bom in child_boms)
|
||||||
|
|
||||||
|
dependants_map, dependency_map = _generate_dependence_map()
|
||||||
|
|
||||||
|
dependants = []
|
||||||
|
for bom in child_boms:
|
||||||
|
# generate list of immediate dependants
|
||||||
|
parents = dependants_map.get(bom) or []
|
||||||
|
dependants.extend(parents)
|
||||||
|
|
||||||
|
dependants = set(dependants) # remove duplicates
|
||||||
|
resolved_dependants = set()
|
||||||
|
|
||||||
|
# consider only if children are all resolved
|
||||||
|
for parent_bom in dependants:
|
||||||
|
if _all_children_are_processed(parent_bom):
|
||||||
|
resolved_dependants.add(parent_bom)
|
||||||
|
|
||||||
|
return list(resolved_dependants)
|
||||||
|
|
||||||
|
|
||||||
|
def get_leaf_boms() -> List[str]:
|
||||||
|
"Get BOMs that have no dependencies."
|
||||||
|
|
||||||
|
return frappe.db.sql_list(
|
||||||
|
"""select name from `tabBOM` bom
|
||||||
|
where docstatus=1 and is_active=1
|
||||||
|
and not exists(select bom_no from `tabBOM Item`
|
||||||
|
where parent=bom.name and ifnull(bom_no, '')!='')"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_dependence_map() -> defaultdict:
|
||||||
|
"""
|
||||||
|
Generate maps such as: { BOM-1: [Dependant-BOM-1, Dependant-BOM-2, ..] }.
|
||||||
|
Here BOM-1 is the leaf/lower level node/dependency.
|
||||||
|
The list contains one level higher nodes/dependants that depend on BOM-1.
|
||||||
|
|
||||||
|
Generate and return the reverse as well.
|
||||||
|
"""
|
||||||
|
|
||||||
|
bom = frappe.qb.DocType("BOM")
|
||||||
|
bom_item = frappe.qb.DocType("BOM Item")
|
||||||
|
|
||||||
|
bom_items = (
|
||||||
|
frappe.qb.from_(bom_item)
|
||||||
|
.join(bom)
|
||||||
|
.on(bom_item.parent == bom.name)
|
||||||
|
.select(bom_item.bom_no, bom_item.parent)
|
||||||
|
.where(
|
||||||
|
(bom_item.bom_no.isnotnull())
|
||||||
|
& (bom_item.bom_no != "")
|
||||||
|
& (bom.docstatus == 1)
|
||||||
|
& (bom.is_active == 1)
|
||||||
|
& (bom_item.parenttype == "BOM")
|
||||||
|
)
|
||||||
|
).run(as_dict=True)
|
||||||
|
|
||||||
|
child_parent_map = defaultdict(list)
|
||||||
|
parent_child_map = defaultdict(list)
|
||||||
|
for row in bom_items:
|
||||||
|
child_parent_map[row.bom_no].append(row.parent)
|
||||||
|
parent_child_map[row.parent].append(row.bom_no)
|
||||||
|
|
||||||
|
return child_parent_map, parent_child_map
|
||||||
|
|
||||||
|
|
||||||
|
def set_values_in_log(log_name: str, values: Dict[str, Any], commit: bool = False) -> None:
|
||||||
|
"Update BOM Update Log record."
|
||||||
|
|
||||||
|
if not values:
|
||||||
|
return
|
||||||
|
|
||||||
|
bom_update_log = frappe.qb.DocType("BOM Update Log")
|
||||||
|
query = frappe.qb.update(bom_update_log).where(bom_update_log.name == log_name)
|
||||||
|
|
||||||
|
for key, value in values.items():
|
||||||
|
query = query.set(key, value)
|
||||||
|
query.run()
|
||||||
|
|
||||||
|
if commit and not frappe.flags.in_test:
|
||||||
|
frappe.db.commit() # nosemgrep
|
||||||
|
|
||||||
|
|
||||||
|
def handle_exception(doc: "BOMUpdateLog") -> None:
|
||||||
|
"Rolls back and fails BOM Update Log."
|
||||||
|
|
||||||
|
frappe.db.rollback()
|
||||||
|
error_log = doc.log_error("BOM Update Tool Error")
|
||||||
|
set_values_in_log(doc.name, {"status": "Failed", "error_log": error_log.name})
|
||||||
@@ -6,9 +6,12 @@ from frappe.tests.utils import FrappeTestCase
|
|||||||
|
|
||||||
from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import (
|
from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import (
|
||||||
BOMMissingError,
|
BOMMissingError,
|
||||||
run_bom_job,
|
resume_bom_cost_update_jobs,
|
||||||
|
)
|
||||||
|
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import (
|
||||||
|
enqueue_replace_bom,
|
||||||
|
enqueue_update_cost,
|
||||||
)
|
)
|
||||||
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import enqueue_replace_bom
|
|
||||||
|
|
||||||
test_records = frappe.get_test_records("BOM")
|
test_records = frappe.get_test_records("BOM")
|
||||||
|
|
||||||
@@ -31,17 +34,12 @@ class TestBOMUpdateLog(FrappeTestCase):
|
|||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
|
|
||||||
if self._testMethodName == "test_bom_update_log_completion":
|
|
||||||
# clear logs and delete BOM created via setUp
|
|
||||||
frappe.db.delete("BOM Update Log")
|
|
||||||
self.new_bom_doc.cancel()
|
|
||||||
self.new_bom_doc.delete()
|
|
||||||
|
|
||||||
# explicitly commit and restore to original state
|
|
||||||
frappe.db.commit() # nosemgrep
|
|
||||||
|
|
||||||
def test_bom_update_log_validate(self):
|
def test_bom_update_log_validate(self):
|
||||||
"Test if BOM presence is validated."
|
"""
|
||||||
|
1) Test if BOM presence is validated.
|
||||||
|
2) Test if same BOMs are validated.
|
||||||
|
3) Test of non-existent BOM is validated.
|
||||||
|
"""
|
||||||
|
|
||||||
with self.assertRaises(BOMMissingError):
|
with self.assertRaises(BOMMissingError):
|
||||||
enqueue_replace_bom(boms={})
|
enqueue_replace_bom(boms={})
|
||||||
@@ -52,45 +50,22 @@ class TestBOMUpdateLog(FrappeTestCase):
|
|||||||
with self.assertRaises(frappe.ValidationError):
|
with self.assertRaises(frappe.ValidationError):
|
||||||
enqueue_replace_bom(boms=frappe._dict(current_bom=self.boms.new_bom, new_bom="Dummy BOM"))
|
enqueue_replace_bom(boms=frappe._dict(current_bom=self.boms.new_bom, new_bom="Dummy BOM"))
|
||||||
|
|
||||||
def test_bom_update_log_queueing(self):
|
|
||||||
"Test if BOM Update Log is created and queued."
|
|
||||||
|
|
||||||
log = enqueue_replace_bom(
|
|
||||||
boms=self.boms,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(log.docstatus, 1)
|
|
||||||
self.assertEqual(log.status, "Queued")
|
|
||||||
|
|
||||||
def test_bom_update_log_completion(self):
|
def test_bom_update_log_completion(self):
|
||||||
"Test if BOM Update Log handles job completion correctly."
|
"Test if BOM Update Log handles job completion correctly."
|
||||||
|
|
||||||
log = enqueue_replace_bom(
|
log = enqueue_replace_bom(boms=self.boms)
|
||||||
boms=self.boms,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Explicitly commits log, new bom (setUp) and replacement impact.
|
|
||||||
# Is run via background jobs IRL
|
|
||||||
run_bom_job(
|
|
||||||
doc=log,
|
|
||||||
boms=self.boms,
|
|
||||||
update_type="Replace BOM",
|
|
||||||
)
|
|
||||||
log.reload()
|
log.reload()
|
||||||
|
|
||||||
self.assertEqual(log.status, "Completed")
|
self.assertEqual(log.status, "Completed")
|
||||||
|
|
||||||
# teardown (undo replace impact) due to commit
|
|
||||||
boms = frappe._dict(
|
def update_cost_in_all_boms_in_test():
|
||||||
current_bom=self.boms.new_bom,
|
"""
|
||||||
new_bom=self.boms.current_bom,
|
Utility to run 'Update Cost' job in tests without Cron job until fully complete.
|
||||||
)
|
"""
|
||||||
log2 = enqueue_replace_bom(
|
log = enqueue_update_cost() # create BOM Update Log
|
||||||
boms=self.boms,
|
|
||||||
)
|
while log.status != "Completed":
|
||||||
run_bom_job( # Explicitly commits
|
resume_bom_cost_update_jobs() # run cron job until complete
|
||||||
doc=log2,
|
log.reload()
|
||||||
boms=boms,
|
|
||||||
update_type="Replace BOM",
|
return log
|
||||||
)
|
|
||||||
self.assertEqual(log2.status, "Completed")
|
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ if TYPE_CHECKING:
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
|
||||||
from erpnext.manufacturing.doctype.bom.bom import get_boms_in_bottom_up_order
|
|
||||||
|
|
||||||
|
|
||||||
class BOMUpdateTool(Document):
|
class BOMUpdateTool(Document):
|
||||||
pass
|
pass
|
||||||
@@ -40,14 +38,13 @@ def enqueue_update_cost() -> "BOMUpdateLog":
|
|||||||
def auto_update_latest_price_in_all_boms() -> None:
|
def auto_update_latest_price_in_all_boms() -> None:
|
||||||
"""Called via hooks.py."""
|
"""Called via hooks.py."""
|
||||||
if frappe.db.get_single_value("Manufacturing Settings", "update_bom_costs_automatically"):
|
if frappe.db.get_single_value("Manufacturing Settings", "update_bom_costs_automatically"):
|
||||||
update_cost()
|
wip_log = frappe.get_all(
|
||||||
|
"BOM Update Log",
|
||||||
|
{"update_type": "Update Cost", "status": ["in", ["Queued", "In Progress"]]},
|
||||||
def update_cost() -> None:
|
limit_page_length=1,
|
||||||
"""Updates Cost for all BOMs from bottom to top."""
|
)
|
||||||
bom_list = get_boms_in_bottom_up_order()
|
if not wip_log:
|
||||||
for bom in bom_list:
|
create_bom_update_log(update_type="Update Cost")
|
||||||
frappe.get_doc("BOM", bom).update_cost(update_parent=False, from_child_bom=True)
|
|
||||||
|
|
||||||
|
|
||||||
def create_bom_update_log(
|
def create_bom_update_log(
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
from frappe.tests.utils import FrappeTestCase
|
||||||
|
|
||||||
from erpnext.manufacturing.doctype.bom_update_log.bom_update_log import replace_bom
|
from erpnext.manufacturing.doctype.bom_update_log.test_bom_update_log import (
|
||||||
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
|
update_cost_in_all_boms_in_test,
|
||||||
|
)
|
||||||
|
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import enqueue_replace_bom
|
||||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||||
from erpnext.stock.doctype.item.test_item import create_item
|
from erpnext.stock.doctype.item.test_item import create_item
|
||||||
|
|
||||||
@@ -15,6 +17,9 @@ test_records = frappe.get_test_records("BOM")
|
|||||||
class TestBOMUpdateTool(FrappeTestCase):
|
class TestBOMUpdateTool(FrappeTestCase):
|
||||||
"Test major functions run via BOM Update Tool."
|
"Test major functions run via BOM Update Tool."
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
frappe.db.rollback()
|
||||||
|
|
||||||
def test_replace_bom(self):
|
def test_replace_bom(self):
|
||||||
current_bom = "BOM-_Test Item Home Desktop Manufactured-001"
|
current_bom = "BOM-_Test Item Home Desktop Manufactured-001"
|
||||||
|
|
||||||
@@ -23,15 +28,10 @@ class TestBOMUpdateTool(FrappeTestCase):
|
|||||||
bom_doc.insert()
|
bom_doc.insert()
|
||||||
|
|
||||||
boms = frappe._dict(current_bom=current_bom, new_bom=bom_doc.name)
|
boms = frappe._dict(current_bom=current_bom, new_bom=bom_doc.name)
|
||||||
replace_bom(boms)
|
enqueue_replace_bom(boms=boms)
|
||||||
|
|
||||||
self.assertFalse(frappe.db.sql("select name from `tabBOM Item` where bom_no=%s", current_bom))
|
self.assertFalse(frappe.db.exists("BOM Item", {"bom_no": current_bom, "docstatus": 1}))
|
||||||
self.assertTrue(frappe.db.sql("select name from `tabBOM Item` where bom_no=%s", bom_doc.name))
|
self.assertTrue(frappe.db.exists("BOM Item", {"bom_no": bom_doc.name, "docstatus": 1}))
|
||||||
|
|
||||||
# reverse, as it affects other testcases
|
|
||||||
boms.current_bom = bom_doc.name
|
|
||||||
boms.new_bom = current_bom
|
|
||||||
replace_bom(boms)
|
|
||||||
|
|
||||||
def test_bom_cost(self):
|
def test_bom_cost(self):
|
||||||
for item in ["BOM Cost Test Item 1", "BOM Cost Test Item 2", "BOM Cost Test Item 3"]:
|
for item in ["BOM Cost Test Item 1", "BOM Cost Test Item 2", "BOM Cost Test Item 3"]:
|
||||||
@@ -52,13 +52,13 @@ class TestBOMUpdateTool(FrappeTestCase):
|
|||||||
self.assertEqual(doc.total_cost, 200)
|
self.assertEqual(doc.total_cost, 200)
|
||||||
|
|
||||||
frappe.db.set_value("Item", "BOM Cost Test Item 2", "valuation_rate", 200)
|
frappe.db.set_value("Item", "BOM Cost Test Item 2", "valuation_rate", 200)
|
||||||
update_cost()
|
update_cost_in_all_boms_in_test()
|
||||||
|
|
||||||
doc.load_from_db()
|
doc.load_from_db()
|
||||||
self.assertEqual(doc.total_cost, 300)
|
self.assertEqual(doc.total_cost, 300)
|
||||||
|
|
||||||
frappe.db.set_value("Item", "BOM Cost Test Item 2", "valuation_rate", 100)
|
frappe.db.set_value("Item", "BOM Cost Test Item 2", "valuation_rate", 100)
|
||||||
update_cost()
|
update_cost_in_all_boms_in_test()
|
||||||
|
|
||||||
doc.load_from_db()
|
doc.load_from_db()
|
||||||
self.assertEqual(doc.total_cost, 200)
|
self.assertEqual(doc.total_cost, 200)
|
||||||
|
|||||||
@@ -621,19 +621,20 @@ class JobCard(Document):
|
|||||||
self.set_status(update_status)
|
self.set_status(update_status)
|
||||||
|
|
||||||
def set_status(self, update_status=False):
|
def set_status(self, update_status=False):
|
||||||
if self.status == "On Hold":
|
if self.status == "On Hold" and self.docstatus == 0:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.status = {0: "Open", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0]
|
self.status = {0: "Open", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0]
|
||||||
|
|
||||||
if self.for_quantity <= self.transferred_qty:
|
if self.docstatus < 2:
|
||||||
self.status = "Material Transferred"
|
if self.for_quantity <= self.transferred_qty:
|
||||||
|
self.status = "Material Transferred"
|
||||||
|
|
||||||
if self.time_logs:
|
if self.time_logs:
|
||||||
self.status = "Work In Progress"
|
self.status = "Work In Progress"
|
||||||
|
|
||||||
if self.docstatus == 1 and (self.for_quantity <= self.total_completed_qty or not self.items):
|
if self.docstatus == 1 and (self.for_quantity <= self.total_completed_qty or not self.items):
|
||||||
self.status = "Completed"
|
self.status = "Completed"
|
||||||
|
|
||||||
if update_status:
|
if update_status:
|
||||||
self.db_set("status", self.status)
|
self.db_set("status", self.status)
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
frappe.listview_settings['Job Card'] = {
|
frappe.listview_settings['Job Card'] = {
|
||||||
has_indicator_for_draft: true,
|
has_indicator_for_draft: true,
|
||||||
|
|
||||||
get_indicator: function(doc) {
|
get_indicator: function(doc) {
|
||||||
if (doc.status === "Work In Progress") {
|
const status_colors = {
|
||||||
return [__("Work In Progress"), "orange", "status,=,Work In Progress"];
|
"Work In Progress": "orange",
|
||||||
} else if (doc.status === "Completed") {
|
"Completed": "green",
|
||||||
return [__("Completed"), "green", "status,=,Completed"];
|
"Cancelled": "red",
|
||||||
} else if (doc.docstatus == 2) {
|
"Material Transferred": "blue",
|
||||||
return [__("Cancelled"), "red", "status,=,Cancelled"];
|
"Open": "red",
|
||||||
} else if (doc.status === "Material Transferred") {
|
};
|
||||||
return [__('Material Transferred'), "blue", "status,=,Material Transferred"];
|
const status = doc.status || "Open";
|
||||||
} else {
|
const color = status_colors[status] || "blue";
|
||||||
return [__("Open"), "red", "status,=,Open"];
|
|
||||||
}
|
return [__(status), color, `status,=,${status}`];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -344,6 +344,30 @@ class TestJobCard(FrappeTestCase):
|
|||||||
cost_after_cancel = self.work_order.total_operating_cost
|
cost_after_cancel = self.work_order.total_operating_cost
|
||||||
self.assertEqual(cost_after_cancel, original_cost)
|
self.assertEqual(cost_after_cancel, original_cost)
|
||||||
|
|
||||||
|
def test_job_card_statuses(self):
|
||||||
|
def assertStatus(status):
|
||||||
|
jc.set_status()
|
||||||
|
self.assertEqual(jc.status, status)
|
||||||
|
|
||||||
|
jc = frappe.new_doc("Job Card")
|
||||||
|
jc.for_quantity = 2
|
||||||
|
jc.transferred_qty = 1
|
||||||
|
jc.total_completed_qty = 0
|
||||||
|
assertStatus("Open")
|
||||||
|
|
||||||
|
jc.transferred_qty = jc.for_quantity
|
||||||
|
assertStatus("Material Transferred")
|
||||||
|
|
||||||
|
jc.append("time_logs", {})
|
||||||
|
assertStatus("Work In Progress")
|
||||||
|
|
||||||
|
jc.docstatus = 1
|
||||||
|
jc.total_completed_qty = jc.for_quantity
|
||||||
|
assertStatus("Completed")
|
||||||
|
|
||||||
|
jc.docstatus = 2
|
||||||
|
assertStatus("Cancelled")
|
||||||
|
|
||||||
|
|
||||||
def create_bom_with_multiple_operations():
|
def create_bom_with_multiple_operations():
|
||||||
"Create a BOM with multiple operations and Material Transfer against Job Card"
|
"Create a BOM with multiple operations and Material Transfer against Job Card"
|
||||||
|
|||||||
@@ -798,7 +798,6 @@ def make_bom(**args):
|
|||||||
|
|
||||||
for item in args.raw_materials:
|
for item in args.raw_materials:
|
||||||
item_doc = frappe.get_doc("Item", item)
|
item_doc = frappe.get_doc("Item", item)
|
||||||
|
|
||||||
bom.append(
|
bom.append(
|
||||||
"items",
|
"items",
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -421,7 +421,7 @@ class TestWorkOrder(FrappeTestCase):
|
|||||||
"doctype": "Item Price",
|
"doctype": "Item Price",
|
||||||
"item_code": "_Test FG Non Stock Item",
|
"item_code": "_Test FG Non Stock Item",
|
||||||
"price_list_rate": 1000,
|
"price_list_rate": 1000,
|
||||||
"price_list": "Standard Buying",
|
"price_list": "_Test Price List India",
|
||||||
}
|
}
|
||||||
).insert(ignore_permissions=True)
|
).insert(ignore_permissions=True)
|
||||||
|
|
||||||
@@ -430,8 +430,17 @@ class TestWorkOrder(FrappeTestCase):
|
|||||||
item_code="_Test FG Item", target="_Test Warehouse - _TC", qty=1, basic_rate=100
|
item_code="_Test FG Item", target="_Test Warehouse - _TC", qty=1, basic_rate=100
|
||||||
)
|
)
|
||||||
|
|
||||||
if not frappe.db.get_value("BOM", {"item": fg_item}):
|
if not frappe.db.get_value("BOM", {"item": fg_item, "docstatus": 1}):
|
||||||
make_bom(item=fg_item, rate=1000, raw_materials=["_Test FG Item", "_Test FG Non Stock Item"])
|
bom = make_bom(
|
||||||
|
item=fg_item,
|
||||||
|
rate=1000,
|
||||||
|
raw_materials=["_Test FG Item", "_Test FG Non Stock Item"],
|
||||||
|
do_not_save=True,
|
||||||
|
)
|
||||||
|
bom.rm_cost_as_per = "Price List" # non stock item won't have valuation rate
|
||||||
|
bom.buying_price_list = "_Test Price List India"
|
||||||
|
bom.currency = "INR"
|
||||||
|
bom.save()
|
||||||
|
|
||||||
wo = make_wo_order_test_record(production_item=fg_item)
|
wo = make_wo_order_test_record(production_item=fg_item)
|
||||||
|
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ def get_work_orders(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
return frappe.db.sql(
|
return frappe.db.sql(
|
||||||
"""select name from `tabWork Order`
|
"""select name from `tabWork Order`
|
||||||
where name like %(name)s and {0} and produced_qty > qty and docstatus = 1
|
where name like %(name)s and {0} and produced_qty > qty and docstatus = 1
|
||||||
order by name limit {1}, {2}""".format(
|
order by name limit {2} offset {1}""".format(
|
||||||
cond, start, page_len
|
cond, start, page_len
|
||||||
),
|
),
|
||||||
{"name": "%%%s%%" % txt},
|
{"name": "%%%s%%" % txt},
|
||||||
|
|||||||
@@ -373,3 +373,4 @@ erpnext.patches.v13_0.create_accounting_dimensions_in_orders
|
|||||||
erpnext.patches.v13_0.set_per_billed_in_return_delivery_note
|
erpnext.patches.v13_0.set_per_billed_in_return_delivery_note
|
||||||
execute:frappe.delete_doc("DocType", "Naming Series")
|
execute:frappe.delete_doc("DocType", "Naming Series")
|
||||||
erpnext.patches.v13_0.set_payroll_entry_status
|
erpnext.patches.v13_0.set_payroll_entry_status
|
||||||
|
erpnext.patches.v13_0.job_card_status_on_hold
|
||||||
|
|||||||
19
erpnext/patches/v13_0/job_card_status_on_hold.py
Normal file
19
erpnext/patches/v13_0/job_card_status_on_hold.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
job_cards = frappe.get_all(
|
||||||
|
"Job Card",
|
||||||
|
{"status": "On Hold", "docstatus": ("!=", 0)},
|
||||||
|
pluck="name",
|
||||||
|
)
|
||||||
|
|
||||||
|
for idx, job_card in enumerate(job_cards):
|
||||||
|
try:
|
||||||
|
doc = frappe.get_doc("Job Card", job_card)
|
||||||
|
doc.set_status()
|
||||||
|
doc.db_set("status", doc.status, update_modified=False)
|
||||||
|
if idx % 100 == 0:
|
||||||
|
frappe.db.commit()
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import qb
|
from frappe import qb
|
||||||
|
from frappe.query_builder import Case
|
||||||
|
from frappe.query_builder.custom import ConstantColumn
|
||||||
|
from frappe.query_builder.functions import IfNull
|
||||||
|
|
||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||||
get_dimensions,
|
get_dimensions,
|
||||||
make_dimension_in_accounting_doctypes,
|
make_dimension_in_accounting_doctypes,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.utils import create_payment_ledger_entry
|
|
||||||
|
|
||||||
|
|
||||||
def create_accounting_dimension_fields():
|
def create_accounting_dimension_fields():
|
||||||
@@ -15,24 +17,119 @@ def create_accounting_dimension_fields():
|
|||||||
make_dimension_in_accounting_doctypes(dimension, ["Payment Ledger Entry"])
|
make_dimension_in_accounting_doctypes(dimension, ["Payment Ledger Entry"])
|
||||||
|
|
||||||
|
|
||||||
def execute():
|
def generate_name_for_payment_ledger_entries(gl_entries):
|
||||||
# create accounting dimension fields in Payment Ledger
|
for index, entry in enumerate(gl_entries, 1):
|
||||||
create_accounting_dimension_fields()
|
entry.name = index
|
||||||
|
|
||||||
|
|
||||||
|
def get_columns():
|
||||||
|
columns = [
|
||||||
|
"name",
|
||||||
|
"creation",
|
||||||
|
"modified",
|
||||||
|
"modified_by",
|
||||||
|
"owner",
|
||||||
|
"docstatus",
|
||||||
|
"posting_date",
|
||||||
|
"account_type",
|
||||||
|
"account",
|
||||||
|
"party_type",
|
||||||
|
"party",
|
||||||
|
"voucher_type",
|
||||||
|
"voucher_no",
|
||||||
|
"against_voucher_type",
|
||||||
|
"against_voucher_no",
|
||||||
|
"amount",
|
||||||
|
"amount_in_account_currency",
|
||||||
|
"account_currency",
|
||||||
|
"company",
|
||||||
|
"cost_center",
|
||||||
|
"due_date",
|
||||||
|
"finance_book",
|
||||||
|
]
|
||||||
|
|
||||||
|
dimensions_and_defaults = get_dimensions()
|
||||||
|
if dimensions_and_defaults:
|
||||||
|
for dimension in dimensions_and_defaults[0]:
|
||||||
|
columns.append(dimension.fieldname)
|
||||||
|
|
||||||
|
return columns
|
||||||
|
|
||||||
|
|
||||||
|
def build_insert_query():
|
||||||
|
ple = qb.DocType("Payment Ledger Entry")
|
||||||
|
columns = get_columns()
|
||||||
|
insert_query = qb.into(ple)
|
||||||
|
|
||||||
|
# build 'insert' columns in query
|
||||||
|
insert_query = insert_query.columns(tuple(columns))
|
||||||
|
|
||||||
|
return insert_query
|
||||||
|
|
||||||
|
|
||||||
|
def insert_chunk_into_payment_ledger(insert_query, gl_entries):
|
||||||
|
if gl_entries:
|
||||||
|
columns = get_columns()
|
||||||
|
|
||||||
|
# build tuple of data with same column order
|
||||||
|
for entry in gl_entries:
|
||||||
|
data = ()
|
||||||
|
for column in columns:
|
||||||
|
data += (entry[column],)
|
||||||
|
insert_query = insert_query.insert(data)
|
||||||
|
insert_query.run()
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
if frappe.reload_doc("accounts", "doctype", "payment_ledger_entry"):
|
||||||
|
# create accounting dimension fields in Payment Ledger
|
||||||
|
create_accounting_dimension_fields()
|
||||||
|
|
||||||
|
gl = qb.DocType("GL Entry")
|
||||||
|
account = qb.DocType("Account")
|
||||||
|
|
||||||
gl = qb.DocType("GL Entry")
|
|
||||||
accounts = frappe.db.get_list(
|
|
||||||
"Account", "name", filters={"account_type": ["in", ["Receivable", "Payable"]]}, as_list=True
|
|
||||||
)
|
|
||||||
gl_entries = []
|
|
||||||
if accounts:
|
|
||||||
# get all gl entries on receivable/payable accounts
|
|
||||||
gl_entries = (
|
gl_entries = (
|
||||||
qb.from_(gl)
|
qb.from_(gl)
|
||||||
.select("*")
|
.inner_join(account)
|
||||||
.where(gl.account.isin(accounts))
|
.on((gl.account == account.name) & (account.account_type.isin(["Receivable", "Payable"])))
|
||||||
|
.select(
|
||||||
|
gl.star,
|
||||||
|
ConstantColumn(1).as_("docstatus"),
|
||||||
|
account.account_type.as_("account_type"),
|
||||||
|
IfNull(gl.against_voucher_type, gl.voucher_type).as_("against_voucher_type"),
|
||||||
|
IfNull(gl.against_voucher, gl.voucher_no).as_("against_voucher_no"),
|
||||||
|
# convert debit/credit to amount
|
||||||
|
Case()
|
||||||
|
.when(account.account_type == "Receivable", gl.debit - gl.credit)
|
||||||
|
.else_(gl.credit - gl.debit)
|
||||||
|
.as_("amount"),
|
||||||
|
# convert debit/credit in account currency to amount in account currency
|
||||||
|
Case()
|
||||||
|
.when(
|
||||||
|
account.account_type == "Receivable",
|
||||||
|
gl.debit_in_account_currency - gl.credit_in_account_currency,
|
||||||
|
)
|
||||||
|
.else_(gl.credit_in_account_currency - gl.debit_in_account_currency)
|
||||||
|
.as_("amount_in_account_currency"),
|
||||||
|
)
|
||||||
.where(gl.is_cancelled == 0)
|
.where(gl.is_cancelled == 0)
|
||||||
|
.orderby(gl.creation)
|
||||||
.run(as_dict=True)
|
.run(as_dict=True)
|
||||||
)
|
)
|
||||||
if gl_entries:
|
|
||||||
# create payment ledger entries for the accounts receivable/payable
|
# primary key(name) for payment ledger records
|
||||||
create_payment_ledger_entry(gl_entries, 0)
|
generate_name_for_payment_ledger_entries(gl_entries)
|
||||||
|
|
||||||
|
# split data into chunks
|
||||||
|
chunk_size = 1000
|
||||||
|
try:
|
||||||
|
for i in range(0, len(gl_entries), chunk_size):
|
||||||
|
insert_query = build_insert_query()
|
||||||
|
insert_chunk_into_payment_ledger(insert_query, gl_entries[i : i + chunk_size])
|
||||||
|
frappe.db.commit()
|
||||||
|
except Exception as err:
|
||||||
|
frappe.db.rollback()
|
||||||
|
ple = qb.DocType("Payment Ledger Entry")
|
||||||
|
qb.from_(ple).delete().where(ple.docstatus >= 0).run()
|
||||||
|
frappe.db.commit()
|
||||||
|
raise err
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import add_days, cint, cstr, date_diff, getdate, rounded
|
from frappe.utils import add_days, cstr, date_diff, flt, getdate, rounded
|
||||||
|
|
||||||
from erpnext.hr.utils import (
|
from erpnext.hr.utils import (
|
||||||
get_holiday_dates_for_employee,
|
get_holiday_dates_for_employee,
|
||||||
@@ -27,11 +27,14 @@ class EmployeeBenefitApplication(Document):
|
|||||||
validate_active_employee(self.employee)
|
validate_active_employee(self.employee)
|
||||||
self.validate_duplicate_on_payroll_period()
|
self.validate_duplicate_on_payroll_period()
|
||||||
if not self.max_benefits:
|
if not self.max_benefits:
|
||||||
self.max_benefits = get_max_benefits_remaining(self.employee, self.date, self.payroll_period)
|
self.max_benefits = flt(
|
||||||
|
get_max_benefits_remaining(self.employee, self.date, self.payroll_period),
|
||||||
|
self.precision("max_benefits"),
|
||||||
|
)
|
||||||
if self.max_benefits and self.max_benefits > 0:
|
if self.max_benefits and self.max_benefits > 0:
|
||||||
self.validate_max_benefit_for_component()
|
self.validate_max_benefit_for_component()
|
||||||
self.validate_prev_benefit_claim()
|
self.validate_prev_benefit_claim()
|
||||||
if self.remaining_benefit > 0:
|
if self.remaining_benefit and self.remaining_benefit > 0:
|
||||||
self.validate_remaining_benefit_amount()
|
self.validate_remaining_benefit_amount()
|
||||||
else:
|
else:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
@@ -110,7 +113,7 @@ class EmployeeBenefitApplication(Document):
|
|||||||
max_benefit_amount = 0
|
max_benefit_amount = 0
|
||||||
for employee_benefit in self.employee_benefits:
|
for employee_benefit in self.employee_benefits:
|
||||||
self.validate_max_benefit(employee_benefit.earning_component)
|
self.validate_max_benefit(employee_benefit.earning_component)
|
||||||
max_benefit_amount += employee_benefit.amount
|
max_benefit_amount += flt(employee_benefit.amount)
|
||||||
if max_benefit_amount > self.max_benefits:
|
if max_benefit_amount > self.max_benefits:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Maximum benefit amount of employee {0} exceeds {1}").format(
|
_("Maximum benefit amount of employee {0} exceeds {1}").format(
|
||||||
@@ -125,7 +128,8 @@ class EmployeeBenefitApplication(Document):
|
|||||||
benefit_amount = 0
|
benefit_amount = 0
|
||||||
for employee_benefit in self.employee_benefits:
|
for employee_benefit in self.employee_benefits:
|
||||||
if employee_benefit.earning_component == earning_component_name:
|
if employee_benefit.earning_component == earning_component_name:
|
||||||
benefit_amount += employee_benefit.amount
|
benefit_amount += flt(employee_benefit.amount)
|
||||||
|
|
||||||
prev_sal_slip_flexi_amount = get_sal_slip_total_benefit_given(
|
prev_sal_slip_flexi_amount = get_sal_slip_total_benefit_given(
|
||||||
self.employee, frappe.get_doc("Payroll Period", self.payroll_period), earning_component_name
|
self.employee, frappe.get_doc("Payroll Period", self.payroll_period), earning_component_name
|
||||||
)
|
)
|
||||||
@@ -207,26 +211,47 @@ def get_max_benefits_remaining(employee, on_date, payroll_period):
|
|||||||
def calculate_lwp(employee, start_date, holidays, working_days):
|
def calculate_lwp(employee, start_date, holidays, working_days):
|
||||||
lwp = 0
|
lwp = 0
|
||||||
holidays = "','".join(holidays)
|
holidays = "','".join(holidays)
|
||||||
|
|
||||||
for d in range(working_days):
|
for d in range(working_days):
|
||||||
dt = add_days(cstr(getdate(start_date)), d)
|
date = add_days(cstr(getdate(start_date)), d)
|
||||||
leave = frappe.db.sql(
|
|
||||||
"""
|
LeaveApplication = frappe.qb.DocType("Leave Application")
|
||||||
select t1.name, t1.half_day
|
LeaveType = frappe.qb.DocType("Leave Type")
|
||||||
from `tabLeave Application` t1, `tabLeave Type` t2
|
|
||||||
where t2.name = t1.leave_type
|
is_half_day = (
|
||||||
and t2.is_lwp = 1
|
frappe.qb.terms.Case()
|
||||||
and t1.docstatus = 1
|
.when(
|
||||||
and t1.employee = %(employee)s
|
(
|
||||||
and CASE WHEN t2.include_holiday != 1 THEN %(dt)s not in ('{0}') and %(dt)s between from_date and to_date
|
(LeaveApplication.half_day_date == date)
|
||||||
WHEN t2.include_holiday THEN %(dt)s between from_date and to_date
|
| (LeaveApplication.from_date == LeaveApplication.to_date)
|
||||||
END
|
),
|
||||||
""".format(
|
LeaveApplication.half_day,
|
||||||
holidays
|
)
|
||||||
),
|
.else_(0)
|
||||||
{"employee": employee, "dt": dt},
|
).as_("is_half_day")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(LeaveApplication)
|
||||||
|
.inner_join(LeaveType)
|
||||||
|
.on((LeaveType.name == LeaveApplication.leave_type))
|
||||||
|
.select(LeaveApplication.name, is_half_day)
|
||||||
|
.where(
|
||||||
|
(LeaveType.is_lwp == 1)
|
||||||
|
& (LeaveApplication.docstatus == 1)
|
||||||
|
& (LeaveApplication.status == "Approved")
|
||||||
|
& (LeaveApplication.employee == employee)
|
||||||
|
& ((LeaveApplication.from_date <= date) & (date <= LeaveApplication.to_date))
|
||||||
|
)
|
||||||
)
|
)
|
||||||
if leave:
|
|
||||||
lwp = cint(leave[0][1]) and (lwp + 0.5) or (lwp + 1)
|
# if it's a holiday only include if leave type has "include holiday" enabled
|
||||||
|
if date in holidays:
|
||||||
|
query = query.where((LeaveType.include_holiday == "1"))
|
||||||
|
leaves = query.run(as_dict=True)
|
||||||
|
|
||||||
|
if leaves:
|
||||||
|
lwp += 0.5 if leaves[0].is_half_day else 1
|
||||||
|
|
||||||
return lwp
|
return lwp
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,82 @@
|
|||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe.tests.utils import FrappeTestCase
|
||||||
|
from frappe.utils import add_days, date_diff, get_year_ending, get_year_start, getdate
|
||||||
|
|
||||||
class TestEmployeeBenefitApplication(unittest.TestCase):
|
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||||
pass
|
from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list
|
||||||
|
from erpnext.hr.doctype.leave_application.test_leave_application import get_first_sunday
|
||||||
|
from erpnext.hr.utils import get_holiday_dates_for_employee
|
||||||
|
from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import (
|
||||||
|
calculate_lwp,
|
||||||
|
)
|
||||||
|
from erpnext.payroll.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration import (
|
||||||
|
create_payroll_period,
|
||||||
|
)
|
||||||
|
from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
|
||||||
|
make_holiday_list,
|
||||||
|
make_leave_application,
|
||||||
|
)
|
||||||
|
from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip
|
||||||
|
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
|
||||||
|
|
||||||
|
|
||||||
|
class TestEmployeeBenefitApplication(FrappeTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
date = getdate()
|
||||||
|
make_holiday_list(from_date=get_year_start(date), to_date=get_year_ending(date))
|
||||||
|
|
||||||
|
@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
|
||||||
|
def test_employee_benefit_application(self):
|
||||||
|
payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company")
|
||||||
|
employee = make_employee("test_employee_benefits@salary.com", company="_Test Company")
|
||||||
|
first_sunday = get_first_sunday("Salary Slip Test Holiday List")
|
||||||
|
|
||||||
|
leave_application = make_leave_application(
|
||||||
|
employee,
|
||||||
|
add_days(first_sunday, 1),
|
||||||
|
add_days(first_sunday, 3),
|
||||||
|
"Leave Without Pay",
|
||||||
|
half_day=1,
|
||||||
|
half_day_date=add_days(first_sunday, 1),
|
||||||
|
submit=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
frappe.db.set_value("Leave Type", "Leave Without Pay", "include_holiday", 0)
|
||||||
|
salary_structure = make_salary_structure(
|
||||||
|
"Test Employee Benefits",
|
||||||
|
"Monthly",
|
||||||
|
other_details={"max_benefits": 100000},
|
||||||
|
include_flexi_benefits=True,
|
||||||
|
employee=employee,
|
||||||
|
payroll_period=payroll_period,
|
||||||
|
)
|
||||||
|
salary_slip = make_salary_slip(salary_structure.name, employee=employee, posting_date=getdate())
|
||||||
|
salary_slip.insert()
|
||||||
|
salary_slip.submit()
|
||||||
|
|
||||||
|
application = make_employee_benefit_application(
|
||||||
|
employee, payroll_period.name, date=leave_application.to_date
|
||||||
|
)
|
||||||
|
self.assertEqual(application.employee_benefits[0].max_benefit_amount, 15000)
|
||||||
|
|
||||||
|
holidays = get_holiday_dates_for_employee(employee, payroll_period.start_date, application.date)
|
||||||
|
working_days = date_diff(application.date, payroll_period.start_date) + 1
|
||||||
|
lwp = calculate_lwp(employee, payroll_period.start_date, holidays, working_days)
|
||||||
|
self.assertEqual(lwp, 2.5)
|
||||||
|
|
||||||
|
|
||||||
|
def make_employee_benefit_application(employee, payroll_period, date):
|
||||||
|
frappe.db.delete("Employee Benefit Application")
|
||||||
|
|
||||||
|
return frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Employee Benefit Application",
|
||||||
|
"employee": employee,
|
||||||
|
"date": date,
|
||||||
|
"payroll_period": payroll_period,
|
||||||
|
"employee_benefits": [{"earning_component": "Medical Allowance", "amount": 1500}],
|
||||||
|
}
|
||||||
|
).insert()
|
||||||
|
|||||||
@@ -970,7 +970,7 @@ def get_payroll_entries_for_jv(doctype, txt, searchfield, start, page_len, filte
|
|||||||
and name not in
|
and name not in
|
||||||
(select reference_name from `tabJournal Entry Account`
|
(select reference_name from `tabJournal Entry Account`
|
||||||
where reference_type="Payroll Entry")
|
where reference_type="Payroll Entry")
|
||||||
order by name limit %(start)s, %(page_len)s""".format(
|
order by name limit %(page_len)s offset %(start)s""".format(
|
||||||
key=searchfield
|
key=searchfield
|
||||||
),
|
),
|
||||||
{"txt": "%%%s%%" % txt, "start": start, "page_len": page_len},
|
{"txt": "%%%s%%" % txt, "start": start, "page_len": page_len},
|
||||||
@@ -1039,7 +1039,7 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
if(locate(%(_txt)s, employee_name), locate(%(_txt)s, employee_name), 99999),
|
if(locate(%(_txt)s, employee_name), locate(%(_txt)s, employee_name), 99999),
|
||||||
idx desc,
|
idx desc,
|
||||||
name, employee_name
|
name, employee_name
|
||||||
limit %(start)s, %(page_len)s""".format(
|
limit %(page_len)s offset %(start)s""".format(
|
||||||
**{
|
**{
|
||||||
"key": searchfield,
|
"key": searchfield,
|
||||||
"fcond": get_filters_cond(doctype, filters, conditions),
|
"fcond": get_filters_cond(doctype, filters, conditions),
|
||||||
|
|||||||
@@ -465,37 +465,14 @@ class SalarySlip(TransactionBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
for d in range(working_days):
|
for d in range(working_days):
|
||||||
dt = add_days(cstr(getdate(self.start_date)), d)
|
date = add_days(cstr(getdate(self.start_date)), d)
|
||||||
leave = frappe.db.sql(
|
leave = get_lwp_or_ppl_for_date(date, self.employee, holidays)
|
||||||
"""
|
|
||||||
SELECT t1.name,
|
|
||||||
CASE WHEN (t1.half_day_date = %(dt)s or t1.to_date = t1.from_date)
|
|
||||||
THEN t1.half_day else 0 END,
|
|
||||||
t2.is_ppl,
|
|
||||||
t2.fraction_of_daily_salary_per_leave
|
|
||||||
FROM `tabLeave Application` t1, `tabLeave Type` t2
|
|
||||||
WHERE t2.name = t1.leave_type
|
|
||||||
AND (t2.is_lwp = 1 or t2.is_ppl = 1)
|
|
||||||
AND t1.docstatus = 1
|
|
||||||
AND t1.employee = %(employee)s
|
|
||||||
AND ifnull(t1.salary_slip, '') = ''
|
|
||||||
AND CASE
|
|
||||||
WHEN t2.include_holiday != 1
|
|
||||||
THEN %(dt)s not in ('{0}') and %(dt)s between from_date and to_date
|
|
||||||
WHEN t2.include_holiday
|
|
||||||
THEN %(dt)s between from_date and to_date
|
|
||||||
END
|
|
||||||
""".format(
|
|
||||||
holidays
|
|
||||||
),
|
|
||||||
{"employee": self.employee, "dt": dt},
|
|
||||||
)
|
|
||||||
|
|
||||||
if leave:
|
if leave:
|
||||||
equivalent_lwp_count = 0
|
equivalent_lwp_count = 0
|
||||||
is_half_day_leave = cint(leave[0][1])
|
is_half_day_leave = cint(leave[0].is_half_day)
|
||||||
is_partially_paid_leave = cint(leave[0][2])
|
is_partially_paid_leave = cint(leave[0].is_ppl)
|
||||||
fraction_of_daily_salary_per_leave = flt(leave[0][3])
|
fraction_of_daily_salary_per_leave = flt(leave[0].fraction_of_daily_salary_per_leave)
|
||||||
|
|
||||||
equivalent_lwp_count = (1 - daily_wages_fraction_for_half_day) if is_half_day_leave else 1
|
equivalent_lwp_count = (1 - daily_wages_fraction_for_half_day) if is_half_day_leave else 1
|
||||||
|
|
||||||
@@ -1742,3 +1719,46 @@ def eval_tax_slab_condition(condition, eval_globals=None, eval_locals=None):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
frappe.throw(_("Error in formula or condition: {0} in Income Tax Slab").format(e))
|
frappe.throw(_("Error in formula or condition: {0} in Income Tax Slab").format(e))
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def get_lwp_or_ppl_for_date(date, employee, holidays):
|
||||||
|
LeaveApplication = frappe.qb.DocType("Leave Application")
|
||||||
|
LeaveType = frappe.qb.DocType("Leave Type")
|
||||||
|
|
||||||
|
is_half_day = (
|
||||||
|
frappe.qb.terms.Case()
|
||||||
|
.when(
|
||||||
|
(
|
||||||
|
(LeaveApplication.half_day_date == date)
|
||||||
|
| (LeaveApplication.from_date == LeaveApplication.to_date)
|
||||||
|
),
|
||||||
|
LeaveApplication.half_day,
|
||||||
|
)
|
||||||
|
.else_(0)
|
||||||
|
).as_("is_half_day")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(LeaveApplication)
|
||||||
|
.inner_join(LeaveType)
|
||||||
|
.on((LeaveType.name == LeaveApplication.leave_type))
|
||||||
|
.select(
|
||||||
|
LeaveApplication.name,
|
||||||
|
LeaveType.is_ppl,
|
||||||
|
LeaveType.fraction_of_daily_salary_per_leave,
|
||||||
|
(is_half_day),
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
(((LeaveType.is_lwp == 1) | (LeaveType.is_ppl == 1)))
|
||||||
|
& (LeaveApplication.docstatus == 1)
|
||||||
|
& (LeaveApplication.status == "Approved")
|
||||||
|
& (LeaveApplication.employee == employee)
|
||||||
|
& ((LeaveApplication.salary_slip.isnull()) | (LeaveApplication.salary_slip == ""))
|
||||||
|
& ((LeaveApplication.from_date <= date) & (date <= LeaveApplication.to_date))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# if it's a holiday only include if leave type has "include holiday" enabled
|
||||||
|
if date in holidays:
|
||||||
|
query = query.where((LeaveType.include_holiday == "1"))
|
||||||
|
|
||||||
|
return query.run(as_dict=True)
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
"Payroll Settings", {"payroll_based_on": "Attendance", "daily_wages_fraction_for_half_day": 0.75}
|
"Payroll Settings", {"payroll_based_on": "Attendance", "daily_wages_fraction_for_half_day": 0.75}
|
||||||
)
|
)
|
||||||
def test_payment_days_based_on_attendance(self):
|
def test_payment_days_based_on_attendance(self):
|
||||||
no_of_days = self.get_no_of_days()
|
no_of_days = get_no_of_days()
|
||||||
|
|
||||||
emp_id = make_employee("test_payment_days_based_on_attendance@salary.com")
|
emp_id = make_employee("test_payment_days_based_on_attendance@salary.com")
|
||||||
frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"})
|
frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"})
|
||||||
@@ -128,7 +128,7 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
def test_payment_days_for_mid_joinee_including_holidays(self):
|
def test_payment_days_for_mid_joinee_including_holidays(self):
|
||||||
no_of_days = self.get_no_of_days()
|
no_of_days = get_no_of_days()
|
||||||
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
|
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
|
||||||
|
|
||||||
new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com")
|
new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com")
|
||||||
@@ -196,7 +196,7 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
# tests mid month joining and relieving along with unmarked days
|
# tests mid month joining and relieving along with unmarked days
|
||||||
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
|
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
|
||||||
|
|
||||||
no_of_days = self.get_no_of_days()
|
no_of_days = get_no_of_days()
|
||||||
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
|
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
|
||||||
|
|
||||||
new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com")
|
new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com")
|
||||||
@@ -236,7 +236,7 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
def test_payment_days_for_mid_joinee_excluding_holidays(self):
|
def test_payment_days_for_mid_joinee_excluding_holidays(self):
|
||||||
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
|
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
|
||||||
|
|
||||||
no_of_days = self.get_no_of_days()
|
no_of_days = get_no_of_days()
|
||||||
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
|
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
|
||||||
|
|
||||||
new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com")
|
new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com")
|
||||||
@@ -267,7 +267,7 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
|
|
||||||
@change_settings("Payroll Settings", {"payroll_based_on": "Leave"})
|
@change_settings("Payroll Settings", {"payroll_based_on": "Leave"})
|
||||||
def test_payment_days_based_on_leave_application(self):
|
def test_payment_days_based_on_leave_application(self):
|
||||||
no_of_days = self.get_no_of_days()
|
no_of_days = get_no_of_days()
|
||||||
|
|
||||||
emp_id = make_employee("test_payment_days_based_on_leave_application@salary.com")
|
emp_id = make_employee("test_payment_days_based_on_leave_application@salary.com")
|
||||||
frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"})
|
frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"})
|
||||||
@@ -366,7 +366,7 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
salary_slip.submit()
|
salary_slip.submit()
|
||||||
salary_slip.reload()
|
salary_slip.reload()
|
||||||
|
|
||||||
no_of_days = self.get_no_of_days()
|
no_of_days = get_no_of_days()
|
||||||
days_in_month = no_of_days[0]
|
days_in_month = no_of_days[0]
|
||||||
no_of_holidays = no_of_days[1]
|
no_of_holidays = no_of_days[1]
|
||||||
|
|
||||||
@@ -441,7 +441,7 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
|
|
||||||
@change_settings("Payroll Settings", {"include_holidays_in_total_working_days": 1})
|
@change_settings("Payroll Settings", {"include_holidays_in_total_working_days": 1})
|
||||||
def test_salary_slip_with_holidays_included(self):
|
def test_salary_slip_with_holidays_included(self):
|
||||||
no_of_days = self.get_no_of_days()
|
no_of_days = get_no_of_days()
|
||||||
make_employee("test_salary_slip_with_holidays_included@salary.com")
|
make_employee("test_salary_slip_with_holidays_included@salary.com")
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
"Employee",
|
"Employee",
|
||||||
@@ -473,7 +473,7 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
|
|
||||||
@change_settings("Payroll Settings", {"include_holidays_in_total_working_days": 0})
|
@change_settings("Payroll Settings", {"include_holidays_in_total_working_days": 0})
|
||||||
def test_salary_slip_with_holidays_excluded(self):
|
def test_salary_slip_with_holidays_excluded(self):
|
||||||
no_of_days = self.get_no_of_days()
|
no_of_days = get_no_of_days()
|
||||||
make_employee("test_salary_slip_with_holidays_excluded@salary.com")
|
make_employee("test_salary_slip_with_holidays_excluded@salary.com")
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
"Employee",
|
"Employee",
|
||||||
@@ -510,7 +510,7 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
create_salary_structure_assignment,
|
create_salary_structure_assignment,
|
||||||
)
|
)
|
||||||
|
|
||||||
no_of_days = self.get_no_of_days()
|
no_of_days = get_no_of_days()
|
||||||
|
|
||||||
# set joinng date in the same month
|
# set joinng date in the same month
|
||||||
employee = make_employee("test_payment_days@salary.com")
|
employee = make_employee("test_payment_days@salary.com")
|
||||||
@@ -984,17 +984,18 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
activity_type.wage_rate = 25
|
activity_type.wage_rate = 25
|
||||||
activity_type.save()
|
activity_type.save()
|
||||||
|
|
||||||
def get_no_of_days(self):
|
|
||||||
no_of_days_in_month = calendar.monthrange(getdate(nowdate()).year, getdate(nowdate()).month)
|
|
||||||
no_of_holidays_in_month = len(
|
|
||||||
[
|
|
||||||
1
|
|
||||||
for i in calendar.monthcalendar(getdate(nowdate()).year, getdate(nowdate()).month)
|
|
||||||
if i[6] != 0
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
return [no_of_days_in_month[1], no_of_holidays_in_month]
|
def get_no_of_days():
|
||||||
|
no_of_days_in_month = calendar.monthrange(getdate(nowdate()).year, getdate(nowdate()).month)
|
||||||
|
no_of_holidays_in_month = len(
|
||||||
|
[
|
||||||
|
1
|
||||||
|
for i in calendar.monthcalendar(getdate(nowdate()).year, getdate(nowdate()).month)
|
||||||
|
if i[6] != 0
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return [no_of_days_in_month[1], no_of_holidays_in_month]
|
||||||
|
|
||||||
|
|
||||||
def make_employee_salary_slip(user, payroll_frequency, salary_structure=None, posting_date=None):
|
def make_employee_salary_slip(user, payroll_frequency, salary_structure=None, posting_date=None):
|
||||||
@@ -1136,6 +1137,7 @@ def make_earning_salary_component(
|
|||||||
"pay_against_benefit_claim": 0,
|
"pay_against_benefit_claim": 0,
|
||||||
"type": "Earning",
|
"type": "Earning",
|
||||||
"max_benefit_amount": 15000,
|
"max_benefit_amount": 15000,
|
||||||
|
"depends_on_payment_days": 1,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -391,7 +391,7 @@ def get_users_for_project(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
if(locate(%(_txt)s, full_name), locate(%(_txt)s, full_name), 99999),
|
if(locate(%(_txt)s, full_name), locate(%(_txt)s, full_name), 99999),
|
||||||
idx desc,
|
idx desc,
|
||||||
name, full_name
|
name, full_name
|
||||||
limit %(start)s, %(page_len)s""".format(
|
limit %(page_len)s offset %(start)s""".format(
|
||||||
**{
|
**{
|
||||||
"key": searchfield,
|
"key": searchfield,
|
||||||
"fcond": get_filters_cond(doctype, filters, conditions),
|
"fcond": get_filters_cond(doctype, filters, conditions),
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ def get_project(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
%(mcond)s
|
%(mcond)s
|
||||||
{search_condition}
|
{search_condition}
|
||||||
order by name
|
order by name
|
||||||
limit %(start)s, %(page_len)s""".format(
|
limit %(page_len)s offset %(start)s""".format(
|
||||||
search_columns=search_columns, search_condition=search_cond
|
search_columns=search_columns, search_condition=search_cond
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -328,7 +328,7 @@ def get_timesheet(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
ts.status in ('Submitted', 'Payslip') and tsd.parent = ts.name and
|
ts.status in ('Submitted', 'Payslip') and tsd.parent = ts.name and
|
||||||
tsd.docstatus = 1 and ts.total_billable_amount > 0
|
tsd.docstatus = 1 and ts.total_billable_amount > 0
|
||||||
and tsd.parent LIKE %(txt)s {condition}
|
and tsd.parent LIKE %(txt)s {condition}
|
||||||
order by tsd.parent limit %(start)s, %(page_len)s""".format(
|
order by tsd.parent limit %(page_len)s offset %(start)s""".format(
|
||||||
condition=condition
|
condition=condition
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
@@ -515,7 +515,7 @@ def get_timesheets_list(
|
|||||||
tsd.project IN %(projects)s
|
tsd.project IN %(projects)s
|
||||||
)
|
)
|
||||||
ORDER BY `end_date` ASC
|
ORDER BY `end_date` ASC
|
||||||
LIMIT {0}, {1}
|
LIMIT {1} offset {0}
|
||||||
""".format(
|
""".format(
|
||||||
limit_start, limit_page_length
|
limit_start, limit_page_length
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ def query_task(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
case when `%s` like %s then 0 else 1 end,
|
case when `%s` like %s then 0 else 1 end,
|
||||||
`%s`,
|
`%s`,
|
||||||
subject
|
subject
|
||||||
limit %s, %s"""
|
limit %s offset %s"""
|
||||||
% (searchfield, "%s", "%s", match_conditions, "%s", searchfield, "%s", searchfield, "%s", "%s"),
|
% (searchfield, "%s", "%s", match_conditions, "%s", searchfield, "%s", searchfield, "%s", "%s"),
|
||||||
(search_string, search_string, order_by_string, order_by_string, start, page_len),
|
(search_string, search_string, order_by_string, order_by_string, page_len, start),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ erpnext.buying.BuyingController = class BuyingController extends erpnext.Transac
|
|||||||
me.frm.set_query('supplier_address', erpnext.queries.address_query);
|
me.frm.set_query('supplier_address', erpnext.queries.address_query);
|
||||||
|
|
||||||
me.frm.set_query('billing_address', erpnext.queries.company_address_query);
|
me.frm.set_query('billing_address', erpnext.queries.company_address_query);
|
||||||
|
erpnext.accounts.dimensions.setup_dimension_filters(me.frm, me.frm.doctype);
|
||||||
|
|
||||||
if(this.frm.fields_dict.supplier) {
|
if(this.frm.fields_dict.supplier) {
|
||||||
this.frm.set_query("supplier", function() {
|
this.frm.set_query("supplier", function() {
|
||||||
|
|||||||
@@ -148,7 +148,6 @@ class GSTR3BReport(Document):
|
|||||||
FROM `tabPurchase Invoice` p , `tabPurchase Invoice Item` i
|
FROM `tabPurchase Invoice` p , `tabPurchase Invoice Item` i
|
||||||
WHERE p.docstatus = 1 and p.name = i.parent
|
WHERE p.docstatus = 1 and p.name = i.parent
|
||||||
and p.is_opening = 'No'
|
and p.is_opening = 'No'
|
||||||
and p.gst_category != 'Registered Composition'
|
|
||||||
and (i.is_nil_exempt = 1 or i.is_non_gst = 1 or p.gst_category = 'Registered Composition') and
|
and (i.is_nil_exempt = 1 or i.is_non_gst = 1 or p.gst_category = 'Registered Composition') and
|
||||||
month(p.posting_date) = %s and year(p.posting_date) = %s
|
month(p.posting_date) = %s and year(p.posting_date) = %s
|
||||||
and p.company = %s and p.company_gstin = %s
|
and p.company = %s and p.company_gstin = %s
|
||||||
@@ -245,11 +244,10 @@ class GSTR3BReport(Document):
|
|||||||
)
|
)
|
||||||
|
|
||||||
for d in item_details:
|
for d in item_details:
|
||||||
if d.item_code not in self.invoice_items.get(d.parent, {}):
|
self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, 0.0)
|
||||||
self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, 0.0)
|
self.invoice_items[d.parent][d.item_code] += d.get("taxable_value", 0) or d.get(
|
||||||
self.invoice_items[d.parent][d.item_code] += d.get("taxable_value", 0) or d.get(
|
"base_net_amount", 0
|
||||||
"base_net_amount", 0
|
)
|
||||||
)
|
|
||||||
|
|
||||||
if d.is_nil_exempt and d.item_code not in self.is_nil_exempt:
|
if d.is_nil_exempt and d.item_code not in self.is_nil_exempt:
|
||||||
self.is_nil_exempt.append(d.item_code)
|
self.is_nil_exempt.append(d.item_code)
|
||||||
@@ -336,7 +334,6 @@ class GSTR3BReport(Document):
|
|||||||
|
|
||||||
def set_outward_taxable_supplies(self):
|
def set_outward_taxable_supplies(self):
|
||||||
inter_state_supply_details = {}
|
inter_state_supply_details = {}
|
||||||
|
|
||||||
for inv, items_based_on_rate in self.items_based_on_tax_rate.items():
|
for inv, items_based_on_rate in self.items_based_on_tax_rate.items():
|
||||||
gst_category = self.invoice_detail_map.get(inv, {}).get("gst_category")
|
gst_category = self.invoice_detail_map.get(inv, {}).get("gst_category")
|
||||||
place_of_supply = (
|
place_of_supply = (
|
||||||
@@ -362,7 +359,6 @@ class GSTR3BReport(Document):
|
|||||||
else:
|
else:
|
||||||
self.report_dict["sup_details"]["osup_det"]["iamt"] += taxable_value * rate / 100
|
self.report_dict["sup_details"]["osup_det"]["iamt"] += taxable_value * rate / 100
|
||||||
self.report_dict["sup_details"]["osup_det"]["txval"] += taxable_value
|
self.report_dict["sup_details"]["osup_det"]["txval"] += taxable_value
|
||||||
|
|
||||||
if (
|
if (
|
||||||
gst_category in ["Unregistered", "Registered Composition", "UIN Holders"]
|
gst_category in ["Unregistered", "Registered Composition", "UIN Holders"]
|
||||||
and self.gst_details.get("gst_state") != place_of_supply.split("-")[1]
|
and self.gst_details.get("gst_state") != place_of_supply.split("-")[1]
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ def validate_eligibility(doc):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
invalid_company = not frappe.db.get_value("E Invoice User", {"company": doc.get("company")})
|
invalid_company = not frappe.db.get_value("E Invoice User", {"company": doc.get("company")})
|
||||||
|
invalid_company_gstin = not frappe.db.get_value(
|
||||||
|
"E Invoice User", {"gstin": doc.get("company_gstin")}
|
||||||
|
)
|
||||||
invalid_supply_type = doc.get("gst_category") not in [
|
invalid_supply_type = doc.get("gst_category") not in [
|
||||||
"Registered Regular",
|
"Registered Regular",
|
||||||
"Registered Composition",
|
"Registered Composition",
|
||||||
@@ -71,6 +74,7 @@ def validate_eligibility(doc):
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
invalid_company
|
invalid_company
|
||||||
|
or invalid_company_gstin
|
||||||
or invalid_supply_type
|
or invalid_supply_type
|
||||||
or company_transaction
|
or company_transaction
|
||||||
or no_taxes_applied
|
or no_taxes_applied
|
||||||
|
|||||||
@@ -287,7 +287,7 @@ def get_regional_address_details(party_details, doctype, company):
|
|||||||
return party_details
|
return party_details
|
||||||
|
|
||||||
if (
|
if (
|
||||||
doctype in ("Sales Invoice", "Delivery Note", "Sales Order")
|
doctype in ("Sales Invoice", "Delivery Note", "Sales Order", "Quotation")
|
||||||
and party_details.company_gstin
|
and party_details.company_gstin
|
||||||
and party_details.company_gstin[:2] != party_details.place_of_supply[:2]
|
and party_details.company_gstin[:2] != party_details.place_of_supply[:2]
|
||||||
) or (
|
) or (
|
||||||
|
|||||||
@@ -1155,8 +1155,11 @@ def get_company_gstins(company):
|
|||||||
.inner_join(links)
|
.inner_join(links)
|
||||||
.on(address.name == links.parent)
|
.on(address.name == links.parent)
|
||||||
.select(address.gstin)
|
.select(address.gstin)
|
||||||
|
.distinct()
|
||||||
.where(links.link_doctype == "Company")
|
.where(links.link_doctype == "Company")
|
||||||
.where(links.link_name == company)
|
.where(links.link_name == company)
|
||||||
|
.where(address.gstin.isnotnull())
|
||||||
|
.where(address.gstin != "")
|
||||||
.run(as_dict=1)
|
.run(as_dict=1)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from frappe.utils.data import fmt_money
|
|||||||
from frappe.utils.jinja import render_template
|
from frappe.utils.jinja import render_template
|
||||||
from frappe.utils.pdf import get_pdf
|
from frappe.utils.pdf import get_pdf
|
||||||
from frappe.utils.print_format import read_multi_pdf
|
from frappe.utils.print_format import read_multi_pdf
|
||||||
from PyPDF2 import PdfFileWriter
|
from PyPDF2 import PdfWriter
|
||||||
|
|
||||||
from erpnext.accounts.utils import get_fiscal_year
|
from erpnext.accounts.utils import get_fiscal_year
|
||||||
|
|
||||||
@@ -106,7 +106,7 @@ def irs_1099_print(filters):
|
|||||||
|
|
||||||
columns, data = execute(filters)
|
columns, data = execute(filters)
|
||||||
template = frappe.get_doc("Print Format", "IRS 1099 Form").html
|
template = frappe.get_doc("Print Format", "IRS 1099 Form").html
|
||||||
output = PdfFileWriter()
|
output = PdfWriter()
|
||||||
|
|
||||||
for row in data:
|
for row in data:
|
||||||
row["fiscal_year"] = fiscal_year
|
row["fiscal_year"] = fiscal_year
|
||||||
|
|||||||
@@ -141,6 +141,9 @@ class Customer(TransactionBase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def validate_internal_customer(self):
|
def validate_internal_customer(self):
|
||||||
|
if not self.is_internal_customer:
|
||||||
|
self.represents_company = ""
|
||||||
|
|
||||||
internal_customer = frappe.db.get_value(
|
internal_customer = frappe.db.get_value(
|
||||||
"Customer",
|
"Customer",
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ def get_new_item_code(doctype, txt, searchfield, start, page_len, filters):
|
|||||||
return frappe.db.sql(
|
return frappe.db.sql(
|
||||||
"""select name, item_name, description from tabItem
|
"""select name, item_name, description from tabItem
|
||||||
where is_stock_item=0 and name not in (select name from `tabProduct Bundle`)
|
where is_stock_item=0 and name not in (select name from `tabProduct Bundle`)
|
||||||
and %s like %s %s limit %s, %s"""
|
and %s like %s %s limit %s offset %s"""
|
||||||
% (searchfield, "%s", get_match_cond(doctype), "%s", "%s"),
|
% (searchfield, "%s", get_match_cond(doctype), "%s", "%s"),
|
||||||
("%%%s%%" % txt, start, page_len),
|
("%%%s%%" % txt, page_len, start),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -20,6 +20,20 @@ frappe.ui.form.on('Quotation', {
|
|||||||
|
|
||||||
frm.set_df_property('packed_items', 'cannot_add_rows', true);
|
frm.set_df_property('packed_items', 'cannot_add_rows', true);
|
||||||
frm.set_df_property('packed_items', 'cannot_delete_rows', true);
|
frm.set_df_property('packed_items', 'cannot_delete_rows', true);
|
||||||
|
|
||||||
|
frm.set_query('company_address', function(doc) {
|
||||||
|
if(!doc.company) {
|
||||||
|
frappe.throw(__('Please set Company'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
query: 'frappe.contacts.doctype.address.address.address_query',
|
||||||
|
filters: {
|
||||||
|
link_doctype: 'Company',
|
||||||
|
link_name: doc.company
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
refresh: function(frm) {
|
refresh: function(frm) {
|
||||||
@@ -70,7 +84,7 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(doc.docstatus == 1 && doc.status!=='Lost') {
|
if(doc.docstatus == 1 && !(['Lost', 'Ordered']).includes(doc.status)) {
|
||||||
if(!doc.valid_till || frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0) {
|
if(!doc.valid_till || frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0) {
|
||||||
cur_frm.add_custom_button(__('Sales Order'),
|
cur_frm.add_custom_button(__('Sales Order'),
|
||||||
cur_frm.cscript['Make Sales Order'], __('Create'));
|
cur_frm.cscript['Make Sales Order'], __('Create'));
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user