mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-18 12:39:18 +00:00
Compare commits
92 Commits
v13.54.0
...
mergify/bp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4165baa29f | ||
|
|
f5160dc83d | ||
|
|
ab82e30fac | ||
|
|
32f3365ac7 | ||
|
|
80012b7339 | ||
|
|
b049b52294 | ||
|
|
5d1c35634c | ||
|
|
9900274b27 | ||
|
|
14955c70d4 | ||
|
|
8c55e35d20 | ||
|
|
e6e9f1dc26 | ||
|
|
4f8b13ac57 | ||
|
|
f0877ffa47 | ||
|
|
e291b5db3d | ||
|
|
b0f7de1a0f | ||
|
|
8dbb200fe3 | ||
|
|
7df8425756 | ||
|
|
3863c4e7fb | ||
|
|
10f02e60ce | ||
|
|
48eaa51c4a | ||
|
|
fee4eae96c | ||
|
|
ee7c9add39 | ||
|
|
1b78dd17c9 | ||
|
|
77e01ebacf | ||
|
|
bd371e697c | ||
|
|
2c4cee025b | ||
|
|
303becf1e3 | ||
|
|
6f44a1630f | ||
|
|
b7944a7c07 | ||
|
|
3dfc1450a1 | ||
|
|
835c85a087 | ||
|
|
190f77abff | ||
|
|
095d99dbd2 | ||
|
|
a9429e160d | ||
|
|
5342cd0dfa | ||
|
|
3bf84e1464 | ||
|
|
65ae8d9c05 | ||
|
|
35717124cd | ||
|
|
89c107ea8b | ||
|
|
958db77cda | ||
|
|
bc1da4678a | ||
|
|
6cb8a40339 | ||
|
|
9139c14639 | ||
|
|
461eb7a50d | ||
|
|
635c3d54f5 | ||
|
|
1bd3f4eeef | ||
|
|
4b8ed0f6ae | ||
|
|
eea7bbcea7 | ||
|
|
1e436052e2 | ||
|
|
5be5fde276 | ||
|
|
8cb0f690d5 | ||
|
|
4789ecacea | ||
|
|
d00257ffd7 | ||
|
|
37b1a0e778 | ||
|
|
6952f0f082 | ||
|
|
8f4ded6ad1 | ||
|
|
13d5eec194 | ||
|
|
335b6c84db | ||
|
|
00dff0a219 | ||
|
|
b55b428114 | ||
|
|
5669a89afe | ||
|
|
b5b51879ee | ||
|
|
4fc45b035a | ||
|
|
bc907b22d4 | ||
|
|
7e26449b9f | ||
|
|
49d0ab5867 | ||
|
|
ddec91202a | ||
|
|
fe681acaad | ||
|
|
52aff1f703 | ||
|
|
1cfc6cfe5d | ||
|
|
7805c3acf6 | ||
|
|
8b4e69235a | ||
|
|
35a62e2e8d | ||
|
|
f6b35324ef | ||
|
|
66ad823417 | ||
|
|
f674635ecf | ||
|
|
0000c38563 | ||
|
|
23f12f6463 | ||
|
|
6f6db432b5 | ||
|
|
ba7a822682 | ||
|
|
32ae68eb5c | ||
|
|
9eb02637d8 | ||
|
|
fd8543ebd3 | ||
|
|
22fb65621c | ||
|
|
55dfd8e995 | ||
|
|
fa0eef96b9 | ||
|
|
ee42b0a16b | ||
|
|
40e475836e | ||
|
|
1da808a125 | ||
|
|
c86cd99395 | ||
|
|
a3fd4db450 | ||
|
|
76919c4af2 |
34
.github/helper/semgrep_rules/report.yml
vendored
Normal file
34
.github/helper/semgrep_rules/report.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
rules:
|
||||
- id: frappe-missing-translate-function-in-report-python
|
||||
paths:
|
||||
include:
|
||||
- "**/report"
|
||||
exclude:
|
||||
- "**/regional"
|
||||
pattern-either:
|
||||
- patterns:
|
||||
- pattern: |
|
||||
{..., "label": "...", ...}
|
||||
- pattern-not: |
|
||||
{..., "label": _("..."), ...}
|
||||
- patterns:
|
||||
- pattern: dict(..., label="...", ...)
|
||||
- pattern-not: dict(..., label=_("..."), ...)
|
||||
message: |
|
||||
All user facing text must be wrapped in translate function. Please refer to translation documentation. https://frappeframework.com/docs/user/en/guides/basics/translations
|
||||
languages: [python]
|
||||
severity: ERROR
|
||||
|
||||
- id: frappe-translated-values-in-business-logic
|
||||
paths:
|
||||
include:
|
||||
- "**/report"
|
||||
patterns:
|
||||
- pattern-inside: |
|
||||
{..., filters: [...], ...}
|
||||
- pattern: |
|
||||
{..., options: [..., __("..."), ...], ...}
|
||||
message: |
|
||||
Using translated values in options field will require you to translate the values while comparing in business logic. Instead of passing translated labels provide objects that contain both label and value. e.g. { label: __("Option value"), value: "Option value"}
|
||||
languages: [javascript]
|
||||
severity: ERROR
|
||||
16
.github/workflows/linters.yml
vendored
16
.github/workflows/linters.yml
vendored
@@ -11,10 +11,10 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python 3.8
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Install and Run Pre-commit
|
||||
uses: pre-commit/action@v2.0.3
|
||||
@@ -22,10 +22,8 @@ jobs:
|
||||
- name: Download Semgrep rules
|
||||
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules
|
||||
|
||||
- uses: returntocorp/semgrep-action@v1
|
||||
env:
|
||||
SEMGREP_TIMEOUT: 120
|
||||
with:
|
||||
config: >-
|
||||
r/python.lang.correctness
|
||||
./frappe-semgrep-rules/rules
|
||||
- name: Download semgrep
|
||||
run: pip install semgrep
|
||||
|
||||
- name: Run Semgrep rules
|
||||
run: semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness
|
||||
|
||||
@@ -4,7 +4,7 @@ import frappe
|
||||
|
||||
from erpnext.hooks import regional_overrides
|
||||
|
||||
__version__ = "13.54.0"
|
||||
__version__ = "13.54.4"
|
||||
|
||||
|
||||
def get_default_company(user=None):
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
frappe.ui.form.on('Accounts Settings', {
|
||||
refresh: function(frm) {
|
||||
|
||||
},
|
||||
validate_access_key(frm) {
|
||||
frappe.call({
|
||||
doc: frm.doc,
|
||||
method: "validate_access_key"
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -46,6 +46,10 @@
|
||||
"currency_exchange_section",
|
||||
"allow_stale",
|
||||
"stale_days",
|
||||
"service_provider",
|
||||
"column_break_eiyok",
|
||||
"access_key",
|
||||
"validate_access_key",
|
||||
"report_settings_sb",
|
||||
"use_custom_cash_flow"
|
||||
],
|
||||
@@ -282,32 +286,56 @@
|
||||
"label": "Enable Common Party Accounting"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Enabling this will allow creation of multi-currency invoices against single party account in company currency",
|
||||
"fieldname": "allow_multi_currency_invoices_against_single_party_account",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow multi-currency invoices against single party account"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Split Early Payment Discount Loss into Income and Tax Loss",
|
||||
"fieldname": "book_tax_discount_loss",
|
||||
"fieldtype": "Check",
|
||||
"label": "Book Tax Loss on Early Payment Discount"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "show_taxes_as_table_in_print",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Taxes as Table in Print"
|
||||
}
|
||||
"default": "0",
|
||||
"description": "Enabling this will allow creation of multi-currency invoices against single party account in company currency",
|
||||
"fieldname": "allow_multi_currency_invoices_against_single_party_account",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow multi-currency invoices against single party account"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Split Early Payment Discount Loss into Income and Tax Loss",
|
||||
"fieldname": "book_tax_discount_loss",
|
||||
"fieldtype": "Check",
|
||||
"label": "Book Tax Loss on Early Payment Discount"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "show_taxes_as_table_in_print",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show Taxes as Table in Print"
|
||||
},
|
||||
{
|
||||
"default": "frankfurter.app",
|
||||
"fieldname": "service_provider",
|
||||
"fieldtype": "Select",
|
||||
"label": "Service Provider",
|
||||
"options": "frankfurter.app\nexchangerate.host"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.service_provider == \"exchangerate.host\"",
|
||||
"description": "Access Key is mandatory for exchangerate.host",
|
||||
"fieldname": "access_key",
|
||||
"fieldtype": "Data",
|
||||
"label": "Access Key"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.service_provider == \"exchangerate.host\"",
|
||||
"fieldname": "validate_access_key",
|
||||
"fieldtype": "Button",
|
||||
"label": "Validate Access Key"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_eiyok",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-06-13 18:47:46.430291",
|
||||
"modified": "2023-10-07 14:20:01.779208",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -8,12 +8,43 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint
|
||||
from frappe.utils import cint, nowdate
|
||||
|
||||
from erpnext.stock.utils import check_pending_reposting
|
||||
|
||||
|
||||
class AccountsSettings(Document):
|
||||
@frappe.whitelist()
|
||||
def validate_access_key(self):
|
||||
if self.service_provider == "exchangerate.host":
|
||||
if not self.access_key:
|
||||
frappe.throw(_("Access Key is required for exchangerate.host"))
|
||||
else:
|
||||
import requests
|
||||
|
||||
# Validate access key
|
||||
api_url = "https://api.exchangerate.host/convert"
|
||||
response = requests.get(
|
||||
api_url,
|
||||
params={
|
||||
"access_key": self.access_key,
|
||||
"transaction_date": nowdate(),
|
||||
"amount": 1,
|
||||
"from": "USD",
|
||||
"to": "INR",
|
||||
},
|
||||
)
|
||||
# exchangerate.host return 200 for all requests. Can't rely on it to raise exception
|
||||
if not response.json()["success"]:
|
||||
frappe.throw(
|
||||
title=_("Service Provider Error"),
|
||||
msg=_("Currency exchange rate serivce provider: {0} returned Error. {1}").format(
|
||||
frappe.bold(self.service_provider), response.json()
|
||||
),
|
||||
exc=frappe.ValidationError,
|
||||
)
|
||||
frappe.msgprint(msg=_("Success"), title=_("Access Key Validation"))
|
||||
|
||||
def on_update(self):
|
||||
frappe.clear_cache()
|
||||
|
||||
|
||||
@@ -358,6 +358,7 @@ def update_outstanding_amt(
|
||||
if against_voucher_type in ["Sales Invoice", "Purchase Invoice", "Fees"]:
|
||||
ref_doc = frappe.get_doc(against_voucher_type, against_voucher)
|
||||
|
||||
bal = flt(bal, frappe.get_precision(against_voucher_type, "outstanding_amount"))
|
||||
# Didn't use db_set for optimization purpose
|
||||
ref_doc.outstanding_amount = bal
|
||||
frappe.db.set_value(against_voucher_type, against_voucher, "outstanding_amount", bal)
|
||||
|
||||
@@ -175,52 +175,53 @@ class PaymentEntry(AccountsController):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
|
||||
def validate_allocated_amount_with_latest_data(self):
|
||||
latest_references = get_outstanding_reference_documents(
|
||||
{
|
||||
"posting_date": self.posting_date,
|
||||
"company": self.company,
|
||||
"party_type": self.party_type,
|
||||
"payment_type": self.payment_type,
|
||||
"party": self.party,
|
||||
"party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to,
|
||||
"get_outstanding_invoices": True,
|
||||
"get_orders_to_be_billed": True,
|
||||
}
|
||||
)
|
||||
if self.references:
|
||||
latest_references = get_outstanding_reference_documents(
|
||||
{
|
||||
"posting_date": self.posting_date,
|
||||
"company": self.company,
|
||||
"party_type": self.party_type,
|
||||
"payment_type": self.payment_type,
|
||||
"party": self.party,
|
||||
"party_account": self.paid_from if self.payment_type == "Receive" else self.paid_to,
|
||||
"get_outstanding_invoices": True,
|
||||
"get_orders_to_be_billed": True,
|
||||
}
|
||||
)
|
||||
|
||||
# Group latest_references by (voucher_type, voucher_no)
|
||||
latest_lookup = {}
|
||||
for d in latest_references:
|
||||
d = frappe._dict(d)
|
||||
latest_lookup.update({(d.voucher_type, d.voucher_no): d})
|
||||
# Group latest_references by (voucher_type, voucher_no)
|
||||
latest_lookup = {}
|
||||
for d in latest_references:
|
||||
d = frappe._dict(d)
|
||||
latest_lookup.update({(d.voucher_type, d.voucher_no): d})
|
||||
|
||||
for d in self.get("references"):
|
||||
latest = latest_lookup.get((d.reference_doctype, d.reference_name))
|
||||
for d in self.get("references"):
|
||||
latest = latest_lookup.get((d.reference_doctype, d.reference_name))
|
||||
|
||||
# The reference has already been fully paid
|
||||
if not latest:
|
||||
frappe.throw(
|
||||
_("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name)
|
||||
)
|
||||
# The reference has already been partly paid
|
||||
elif latest.outstanding_amount < latest.invoice_amount and flt(
|
||||
d.outstanding_amount, d.precision("outstanding_amount")
|
||||
) != flt(latest.outstanding_amount, d.precision("outstanding_amount")):
|
||||
# The reference has already been fully paid
|
||||
if not latest:
|
||||
frappe.throw(
|
||||
_("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name)
|
||||
)
|
||||
# The reference has already been partly paid
|
||||
elif latest.outstanding_amount < latest.invoice_amount and flt(
|
||||
d.outstanding_amount, d.precision("outstanding_amount")
|
||||
) != flt(latest.outstanding_amount, d.precision("outstanding_amount")):
|
||||
|
||||
frappe.throw(
|
||||
_(
|
||||
"{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts."
|
||||
).format(_(d.reference_doctype), d.reference_name)
|
||||
)
|
||||
frappe.throw(
|
||||
_(
|
||||
"{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts."
|
||||
).format(_(d.reference_doctype), d.reference_name)
|
||||
)
|
||||
|
||||
fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
|
||||
fail_message = _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.")
|
||||
|
||||
if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
|
||||
# Check for negative outstanding invoices as well
|
||||
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
# Check for negative outstanding invoices as well
|
||||
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount):
|
||||
frappe.throw(fail_message.format(d.idx))
|
||||
|
||||
def delink_advance_entry_references(self):
|
||||
for reference in self.references:
|
||||
|
||||
@@ -295,6 +295,7 @@ class PaymentReconciliation(Document):
|
||||
"amount": pay.get("amount"),
|
||||
"allocated_amount": allocated_amount,
|
||||
"difference_amount": pay.get("difference_amount"),
|
||||
"currency": inv.get("currency"),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
"section_break_5",
|
||||
"difference_amount",
|
||||
"column_break_7",
|
||||
"difference_account"
|
||||
"difference_account",
|
||||
"currency"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -37,7 +38,7 @@
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Allocated Amount",
|
||||
"options": "Currency",
|
||||
"options": "currency",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
@@ -112,7 +113,7 @@
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Unreconciled Amount",
|
||||
"options": "Currency",
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -120,7 +121,7 @@
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1,
|
||||
"label": "Amount",
|
||||
"options": "Currency",
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@@ -129,11 +130,18 @@
|
||||
"hidden": 1,
|
||||
"label": "Reference Row",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Currency",
|
||||
"options": "Currency"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-10-06 11:48:59.616562",
|
||||
"modified": "2023-11-28 16:30:43.344612",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Reconciliation Allocation",
|
||||
|
||||
@@ -704,7 +704,7 @@ def get_pos_reserved_qty(item_code, warehouse):
|
||||
reserved_qty = (
|
||||
frappe.qb.from_(p_inv)
|
||||
.from_(p_item)
|
||||
.select(Sum(p_item.qty).as_("qty"))
|
||||
.select(Sum(p_item.stock_qty).as_("stock_qty"))
|
||||
.where(
|
||||
(p_inv.name == p_item.parent)
|
||||
& (IfNull(p_inv.consolidated_invoice, "") == "")
|
||||
@@ -715,7 +715,7 @@ def get_pos_reserved_qty(item_code, warehouse):
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
return reserved_qty[0].qty or 0 if reserved_qty else 0
|
||||
return flt(reserved_qty[0].stock_qty) if reserved_qty else 0
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -1626,6 +1626,30 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
0,
|
||||
)
|
||||
|
||||
def test_default_cost_center_for_purchase(self):
|
||||
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
||||
|
||||
for c_center in ["_Test Cost Center Selling", "_Test Cost Center Buying"]:
|
||||
create_cost_center(cost_center_name=c_center)
|
||||
|
||||
item = create_item(
|
||||
"_Test Cost Center Item For Purchase",
|
||||
is_stock_item=1,
|
||||
buying_cost_center="_Test Cost Center Buying - _TC",
|
||||
selling_cost_center="_Test Cost Center Selling - _TC",
|
||||
)
|
||||
|
||||
pi = make_purchase_invoice(
|
||||
item=item.name, qty=1, rate=1000, update_stock=True, do_not_submit=True, cost_center=""
|
||||
)
|
||||
|
||||
pi.items[0].cost_center = ""
|
||||
pi.set_missing_values()
|
||||
pi.calculate_taxes_and_totals()
|
||||
pi.save()
|
||||
|
||||
self.assertEqual(pi.items[0].cost_center, "_Test Cost Center Buying - _TC")
|
||||
|
||||
|
||||
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
||||
gl_entries = frappe.db.sql(
|
||||
|
||||
@@ -2472,36 +2472,6 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
self.assertEqual(target_doc.company, "_Test Company 1")
|
||||
self.assertEqual(target_doc.supplier, "_Test Internal Supplier")
|
||||
|
||||
def test_sle_for_target_warehouse(self):
|
||||
se = make_stock_entry(
|
||||
item_code="138-CMS Shoe",
|
||||
target="Finished Goods - _TC",
|
||||
company="_Test Company",
|
||||
qty=1,
|
||||
basic_rate=500,
|
||||
)
|
||||
|
||||
si = frappe.copy_doc(test_records[0])
|
||||
si.update_stock = 1
|
||||
si.set_warehouse = "Finished Goods - _TC"
|
||||
si.set_target_warehouse = "Stores - _TC"
|
||||
si.get("items")[0].warehouse = "Finished Goods - _TC"
|
||||
si.get("items")[0].target_warehouse = "Stores - _TC"
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
sles = frappe.get_all(
|
||||
"Stock Ledger Entry", filters={"voucher_no": si.name}, fields=["name", "actual_qty"]
|
||||
)
|
||||
|
||||
# check if both SLEs are created
|
||||
self.assertEqual(len(sles), 2)
|
||||
self.assertEqual(sum(d.actual_qty for d in sles), 0.0)
|
||||
|
||||
# tear down
|
||||
si.cancel()
|
||||
se.cancel()
|
||||
|
||||
def test_internal_transfer_gl_entry(self):
|
||||
si = create_sales_invoice(
|
||||
company="_Test Company with perpetual inventory",
|
||||
|
||||
@@ -4,12 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _, msgprint, scrub
|
||||
from frappe.contacts.doctype.address.address import (
|
||||
get_address_display,
|
||||
get_company_address,
|
||||
get_default_address,
|
||||
)
|
||||
from frappe.contacts.doctype.contact.contact import get_contact_details
|
||||
from frappe.contacts.doctype.address.address import get_company_address, get_default_address
|
||||
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
|
||||
from frappe.model.utils import get_fetch_values
|
||||
from frappe.utils import (
|
||||
@@ -120,6 +115,7 @@ def _get_party_details(
|
||||
party_address,
|
||||
company_address,
|
||||
shipping_address,
|
||||
ignore_permissions=ignore_permissions,
|
||||
)
|
||||
set_contact_details(party_details, party, party_type)
|
||||
set_other_values(party_details, party, party_type)
|
||||
@@ -183,6 +179,8 @@ def set_address_details(
|
||||
party_address=None,
|
||||
company_address=None,
|
||||
shipping_address=None,
|
||||
*,
|
||||
ignore_permissions=False
|
||||
):
|
||||
billing_address_field = (
|
||||
"customer_address" if party_type == "Lead" else party_type.lower() + "_address"
|
||||
@@ -195,13 +193,17 @@ def set_address_details(
|
||||
get_fetch_values(doctype, billing_address_field, party_details[billing_address_field])
|
||||
)
|
||||
# address display
|
||||
party_details.address_display = get_address_display(party_details[billing_address_field])
|
||||
party_details.address_display = render_address(
|
||||
party_details[billing_address_field], check_permissions=not ignore_permissions
|
||||
)
|
||||
# shipping address
|
||||
if party_type in ["Customer", "Lead"]:
|
||||
party_details.shipping_address_name = shipping_address or get_party_shipping_address(
|
||||
party_type, party.name
|
||||
)
|
||||
party_details.shipping_address = get_address_display(party_details["shipping_address_name"])
|
||||
party_details.shipping_address = render_address(
|
||||
party_details["shipping_address_name"], check_permissions=not ignore_permissions
|
||||
)
|
||||
if doctype:
|
||||
party_details.update(
|
||||
get_fetch_values(doctype, "shipping_address_name", party_details.shipping_address_name)
|
||||
@@ -224,7 +226,9 @@ def set_address_details(
|
||||
party_details.update(
|
||||
{
|
||||
"shipping_address": shipping_address,
|
||||
"shipping_address_display": get_address_display(shipping_address),
|
||||
"shipping_address_display": render_address(
|
||||
shipping_address, check_permissions=not ignore_permissions
|
||||
),
|
||||
**get_fetch_values(doctype, "shipping_address", shipping_address),
|
||||
}
|
||||
)
|
||||
@@ -235,7 +239,8 @@ def set_address_details(
|
||||
{
|
||||
"billing_address": party_details.company_address,
|
||||
"billing_address_display": (
|
||||
party_details.company_address_display or get_address_display(party_details.company_address)
|
||||
party_details.company_address_display
|
||||
or render_address(party_details.company_address, check_permissions=True)
|
||||
),
|
||||
**get_fetch_values(doctype, "billing_address", party_details.company_address),
|
||||
}
|
||||
@@ -277,7 +282,34 @@ def set_contact_details(party_details, party, party_type):
|
||||
}
|
||||
)
|
||||
else:
|
||||
party_details.update(get_contact_details(party_details.contact_person))
|
||||
fields = [
|
||||
"name as contact_person",
|
||||
"salutation",
|
||||
"first_name",
|
||||
"last_name",
|
||||
"email_id as contact_email",
|
||||
"mobile_no as contact_mobile",
|
||||
"phone as contact_phone",
|
||||
"designation as contact_designation",
|
||||
"department as contact_department",
|
||||
]
|
||||
|
||||
contact_details = frappe.db.get_value(
|
||||
"Contact", party_details.contact_person, fields, as_dict=True
|
||||
)
|
||||
|
||||
contact_details.contact_display = " ".join(
|
||||
filter(
|
||||
None,
|
||||
[
|
||||
contact_details.get("salutation"),
|
||||
contact_details.get("first_name"),
|
||||
contact_details.get("last_name"),
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
party_details.update(contact_details)
|
||||
|
||||
|
||||
def set_other_values(party_details, party, party_type):
|
||||
@@ -938,3 +970,13 @@ def add_party_account(party_type, party, company, account):
|
||||
doc.append("accounts", accounts)
|
||||
|
||||
doc.save()
|
||||
|
||||
|
||||
def render_address(address, check_permissions=True):
|
||||
try:
|
||||
from frappe.contacts.doctype.address.address import render_address as _render
|
||||
except ImportError:
|
||||
# Older frappe versions where this function is not available
|
||||
from frappe.contacts.doctype.address.address import get_address_display as _render
|
||||
|
||||
return frappe.call(_render, address, check_permissions=check_permissions)
|
||||
|
||||
@@ -73,13 +73,23 @@ def validate_filters(filters, account_details):
|
||||
if not account_details.get(account):
|
||||
frappe.throw(_("Account {0} does not exists").format(account))
|
||||
|
||||
<<<<<<< HEAD
|
||||
if filters.get("account") and filters.get("group_by") == "Group by Account":
|
||||
filters.account = frappe.parse_json(filters.get("account"))
|
||||
=======
|
||||
if (filters.get("account") and filters.get("group_by") == 'Group by Account'):
|
||||
filters.account = frappe.parse_json(filters.get('account'))
|
||||
>>>>>>> fa819f2fb0 (fix: General Ledger translation issues (#27298))
|
||||
for account in filters.account:
|
||||
if account_details[account].is_group == 0:
|
||||
frappe.throw(_("Can not filter based on Child Account, if grouped by Account"))
|
||||
|
||||
<<<<<<< HEAD
|
||||
if filters.get("voucher_no") and filters.get("group_by") in ["Group by Voucher"]:
|
||||
=======
|
||||
if (filters.get("voucher_no")
|
||||
and filters.get("group_by") in ['Group by Voucher']):
|
||||
>>>>>>> fa819f2fb0 (fix: General Ledger translation issues (#27298))
|
||||
frappe.throw(_("Can not filter based on Voucher No, if grouped by Voucher"))
|
||||
|
||||
if filters.from_date > filters.to_date:
|
||||
@@ -359,7 +369,11 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension
|
||||
# Opening for filtered account
|
||||
data.append(totals.opening)
|
||||
|
||||
<<<<<<< HEAD
|
||||
if filters.get("group_by") != "Group by Voucher (Consolidated)":
|
||||
=======
|
||||
if filters.get("group_by") != 'Group by Voucher (Consolidated)':
|
||||
>>>>>>> fa819f2fb0 (fix: General Ledger translation issues (#27298))
|
||||
for acc, acc_dict in iteritems(gle_map):
|
||||
# acc
|
||||
if acc_dict.entries:
|
||||
@@ -407,10 +421,17 @@ def get_totals_dict():
|
||||
|
||||
|
||||
def group_by_field(group_by):
|
||||
<<<<<<< HEAD
|
||||
if group_by == "Group by Party":
|
||||
return "party"
|
||||
elif group_by in ["Group by Voucher (Consolidated)", "Group by Account"]:
|
||||
return "account"
|
||||
=======
|
||||
if group_by == 'Group by Party':
|
||||
return 'party'
|
||||
elif group_by in ['Group by Voucher (Consolidated)', 'Group by Account']:
|
||||
return 'account'
|
||||
>>>>>>> fa819f2fb0 (fix: General Ledger translation issues (#27298))
|
||||
else:
|
||||
return "voucher_no"
|
||||
|
||||
@@ -500,6 +521,19 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
||||
keylist.append(gle.get(dim))
|
||||
keylist.append(gle.get("cost_center"))
|
||||
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
elif gle.posting_date <= to_date:
|
||||
update_value_in_dict(gle_map[gle.get(group_by)].totals, 'total', gle)
|
||||
update_value_in_dict(totals, 'total', gle)
|
||||
if filters.get("group_by") != 'Group by Voucher (Consolidated)':
|
||||
gle_map[gle.get(group_by)].entries.append(gle)
|
||||
elif filters.get("group_by") == 'Group by Voucher (Consolidated)':
|
||||
keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")]
|
||||
for dim in accounting_dimensions:
|
||||
keylist.append(gle.get(dim))
|
||||
keylist.append(gle.get("cost_center"))
|
||||
>>>>>>> fa819f2fb0 (fix: General Ledger translation issues (#27298))
|
||||
key = tuple(keylist)
|
||||
if key not in consolidated_gle:
|
||||
consolidated_gle.setdefault(key, gle)
|
||||
|
||||
@@ -457,6 +457,8 @@ class GrossProfitGenerator(object):
|
||||
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)
|
||||
if self.filters.get("group_by") == "Sales Person":
|
||||
new_row.allocated_amount += flt(row.allocated_amount, self.currency_precision)
|
||||
new_row = self.set_average_rate(new_row)
|
||||
self.grouped_data.append(new_row)
|
||||
else:
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import ValidationError, _, msgprint
|
||||
from frappe.contacts.doctype.address.address import get_address_display
|
||||
from frappe.contacts.doctype.address.address import render_address
|
||||
from frappe.utils import cint, cstr, flt, getdate
|
||||
|
||||
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
|
||||
@@ -14,7 +14,8 @@ from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
|
||||
from erpnext.controllers.stock_controller import StockController
|
||||
from erpnext.controllers.subcontracting import Subcontracting
|
||||
from erpnext.stock.get_item_details import get_conversion_factor
|
||||
from erpnext.stock.utils import get_incoming_rate
|
||||
from erpnext.stock.stock_ledger import get_previous_sle
|
||||
from erpnext.stock.utils import get_incoming_rate, get_valuation_method
|
||||
|
||||
|
||||
class QtyMismatchError(ValidationError):
|
||||
@@ -186,7 +187,9 @@ class BuyingController(StockController, Subcontracting):
|
||||
|
||||
for address_field, address_display_field in address_dict.items():
|
||||
if self.get(address_field):
|
||||
self.set(address_display_field, get_address_display(self.get(address_field)))
|
||||
self.set(
|
||||
address_display_field, render_address(self.get(address_field), check_permissions=False)
|
||||
)
|
||||
|
||||
def set_total_in_words(self):
|
||||
from frappe.utils import money_in_words
|
||||
@@ -504,9 +507,20 @@ class BuyingController(StockController, Subcontracting):
|
||||
)
|
||||
|
||||
if self.is_return:
|
||||
outgoing_rate = get_rate_for_return(
|
||||
self.doctype, self.name, d.item_code, self.return_against, item_row=d
|
||||
)
|
||||
if get_valuation_method(d.item_code) == "Moving Average":
|
||||
previous_sle = get_previous_sle(
|
||||
{
|
||||
"item_code": d.item_code,
|
||||
"warehouse": d.warehouse,
|
||||
"posting_date": self.posting_date,
|
||||
"posting_time": self.posting_time,
|
||||
}
|
||||
)
|
||||
outgoing_rate = flt(previous_sle.get("valuation_rate"))
|
||||
else:
|
||||
outgoing_rate = get_rate_for_return(
|
||||
self.doctype, self.name, d.item_code, self.return_against, item_row=d
|
||||
)
|
||||
|
||||
sle.update({"outgoing_rate": outgoing_rate, "recalculate_rate": 1})
|
||||
if d.from_warehouse:
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _, bold, throw
|
||||
from frappe.contacts.doctype.address.address import get_address_display
|
||||
from frappe.utils import cint, cstr, flt, get_link_to_form, nowtime
|
||||
|
||||
from erpnext.accounts.party import render_address
|
||||
from erpnext.controllers.accounts_controller import get_taxes_and_charges
|
||||
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
|
||||
from erpnext.controllers.stock_controller import StockController
|
||||
@@ -583,7 +583,9 @@ class SellingController(StockController):
|
||||
|
||||
for address_field, address_display_field in address_dict.items():
|
||||
if self.get(address_field):
|
||||
self.set(address_display_field, get_address_display(self.get(address_field)))
|
||||
self.set(
|
||||
address_display_field, render_address(self.get(address_field), check_permissions=False)
|
||||
)
|
||||
|
||||
def validate_for_duplicate_items(self):
|
||||
check_list, chk_dupl_itm = [], []
|
||||
|
||||
@@ -582,13 +582,21 @@ class StockController(AccountsController):
|
||||
d.stock_uom_rate = d.rate / (d.conversion_factor or 1)
|
||||
|
||||
def validate_internal_transfer(self):
|
||||
if (
|
||||
self.doctype in ("Sales Invoice", "Delivery Note", "Purchase Invoice", "Purchase Receipt")
|
||||
and self.is_internal_transfer()
|
||||
):
|
||||
self.validate_in_transit_warehouses()
|
||||
self.validate_multi_currency()
|
||||
self.validate_packed_items()
|
||||
if self.doctype in ("Sales Invoice", "Delivery Note", "Purchase Invoice", "Purchase Receipt"):
|
||||
if self.is_internal_transfer():
|
||||
self.validate_in_transit_warehouses()
|
||||
self.validate_multi_currency()
|
||||
self.validate_packed_items()
|
||||
else:
|
||||
self.validate_internal_transfer_warehouse()
|
||||
|
||||
def validate_internal_transfer_warehouse(self):
|
||||
for row in self.items:
|
||||
if row.get("target_warehouse"):
|
||||
row.target_warehouse = None
|
||||
|
||||
if row.get("from_warehouse"):
|
||||
row.from_warehouse = None
|
||||
|
||||
def validate_in_transit_warehouses(self):
|
||||
if (
|
||||
@@ -906,8 +914,6 @@ def create_item_wise_repost_entries(voucher_type, voucher_no, allow_zero_rate=Fa
|
||||
|
||||
repost_entry = frappe.new_doc("Repost Item Valuation")
|
||||
repost_entry.based_on = "Item and Warehouse"
|
||||
repost_entry.voucher_type = voucher_type
|
||||
repost_entry.voucher_no = voucher_no
|
||||
|
||||
repost_entry.item_code = sle.item_code
|
||||
repost_entry.warehouse = sle.warehouse
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.query_builder.functions import Concat_ws, Date
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -69,53 +70,41 @@ def get_columns():
|
||||
|
||||
|
||||
def get_data(filters):
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
SELECT
|
||||
`tabLead`.name,
|
||||
`tabLead`.lead_name,
|
||||
`tabLead`.status,
|
||||
`tabLead`.lead_owner,
|
||||
`tabLead`.territory,
|
||||
`tabLead`.source,
|
||||
`tabLead`.email_id,
|
||||
`tabLead`.mobile_no,
|
||||
`tabLead`.phone,
|
||||
`tabLead`.owner,
|
||||
`tabLead`.company,
|
||||
concat_ws(', ',
|
||||
trim(',' from `tabAddress`.address_line1),
|
||||
trim(',' from tabAddress.address_line2)
|
||||
) AS address,
|
||||
`tabAddress`.state,
|
||||
`tabAddress`.pincode,
|
||||
`tabAddress`.country
|
||||
FROM
|
||||
`tabLead` left join `tabDynamic Link` on (
|
||||
`tabLead`.name = `tabDynamic Link`.link_name and
|
||||
`tabDynamic Link`.parenttype = 'Address')
|
||||
left join `tabAddress` on (
|
||||
`tabAddress`.name=`tabDynamic Link`.parent)
|
||||
WHERE
|
||||
company = %(company)s
|
||||
AND DATE(`tabLead`.creation) BETWEEN %(from_date)s AND %(to_date)s
|
||||
{conditions}
|
||||
ORDER BY
|
||||
`tabLead`.creation asc """.format(
|
||||
conditions=get_conditions(filters)
|
||||
),
|
||||
filters,
|
||||
as_dict=1,
|
||||
lead = frappe.qb.DocType("Lead")
|
||||
address = frappe.qb.DocType("Address")
|
||||
dynamic_link = frappe.qb.DocType("Dynamic Link")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(lead)
|
||||
.left_join(dynamic_link)
|
||||
.on((lead.name == dynamic_link.link_name) & (dynamic_link.parenttype == "Address"))
|
||||
.left_join(address)
|
||||
.on(address.name == dynamic_link.parent)
|
||||
.select(
|
||||
lead.name,
|
||||
lead.lead_name,
|
||||
lead.status,
|
||||
lead.lead_owner,
|
||||
lead.territory,
|
||||
lead.source,
|
||||
lead.email_id,
|
||||
lead.mobile_no,
|
||||
lead.phone,
|
||||
lead.owner,
|
||||
lead.company,
|
||||
(Concat_ws(", ", address.address_line1, address.address_line2)).as_("address"),
|
||||
address.state,
|
||||
address.pincode,
|
||||
address.country,
|
||||
)
|
||||
.where(lead.company == filters.company)
|
||||
.where(Date(lead.creation).between(filters.from_date, filters.to_date))
|
||||
)
|
||||
|
||||
|
||||
def get_conditions(filters):
|
||||
conditions = []
|
||||
|
||||
if filters.get("territory"):
|
||||
conditions.append(" and `tabLead`.territory=%(territory)s")
|
||||
query = query.where(lead.territory == filters.get("territory"))
|
||||
|
||||
if filters.get("status"):
|
||||
conditions.append(" and `tabLead`.status=%(status)s")
|
||||
query = query.where(lead.status == filters.get("status"))
|
||||
|
||||
return " ".join(conditions) if conditions else ""
|
||||
return query.run(as_dict=1)
|
||||
|
||||
@@ -17,7 +17,6 @@ from erpnext.e_commerce.shopping_cart.cart import (
|
||||
request_for_quotation,
|
||||
update_cart,
|
||||
)
|
||||
from erpnext.tests.utils import create_test_contact_and_address
|
||||
|
||||
|
||||
class TestShoppingCart(unittest.TestCase):
|
||||
@@ -28,7 +27,6 @@ class TestShoppingCart(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
frappe.set_user("Administrator")
|
||||
create_test_contact_and_address()
|
||||
self.enable_shopping_cart()
|
||||
if not frappe.db.exists("Website Item", {"item_code": "_Test Item"}):
|
||||
make_website_item(frappe.get_cached_doc("Item", "_Test Item"))
|
||||
@@ -46,48 +44,57 @@ class TestShoppingCart(unittest.TestCase):
|
||||
frappe.db.sql("delete from `tabTax Rule`")
|
||||
|
||||
def test_get_cart_new_user(self):
|
||||
self.login_as_new_user()
|
||||
|
||||
self.login_as_customer(
|
||||
"test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer"
|
||||
)
|
||||
create_address_and_contact(
|
||||
address_title="_Test Address for Customer 2",
|
||||
first_name="_Test Contact for Customer 2",
|
||||
email="test_contact_two_customer@example.com",
|
||||
customer="_Test Customer 2",
|
||||
)
|
||||
# test if lead is created and quotation with new lead is fetched
|
||||
quotation = _get_cart_quotation()
|
||||
customer = frappe.get_doc("Customer", "_Test Customer 2")
|
||||
quotation = _get_cart_quotation(party=customer)
|
||||
self.assertEqual(quotation.quotation_to, "Customer")
|
||||
self.assertEqual(
|
||||
quotation.contact_person,
|
||||
frappe.db.get_value("Contact", dict(email_id="test_cart_user@example.com")),
|
||||
frappe.db.get_value("Contact", dict(email_id="test_contact_two_customer@example.com")),
|
||||
)
|
||||
self.assertEqual(quotation.contact_email, frappe.session.user)
|
||||
|
||||
return quotation
|
||||
|
||||
def test_get_cart_customer(self):
|
||||
def validate_quotation():
|
||||
def test_get_cart_customer(self, customer="_Test Customer 2"):
|
||||
def validate_quotation(customer_name):
|
||||
# test if quotation with customer is fetched
|
||||
quotation = _get_cart_quotation()
|
||||
party = frappe.get_doc("Customer", customer_name)
|
||||
quotation = _get_cart_quotation(party=party)
|
||||
self.assertEqual(quotation.quotation_to, "Customer")
|
||||
self.assertEqual(quotation.party_name, "_Test Customer")
|
||||
self.assertEqual(quotation.party_name, customer_name)
|
||||
self.assertEqual(quotation.contact_email, frappe.session.user)
|
||||
return quotation
|
||||
|
||||
self.login_as_customer(
|
||||
"test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer"
|
||||
)
|
||||
validate_quotation()
|
||||
|
||||
self.login_as_customer()
|
||||
quotation = validate_quotation()
|
||||
|
||||
quotation = validate_quotation(customer)
|
||||
return quotation
|
||||
|
||||
def test_add_to_cart(self):
|
||||
self.login_as_customer()
|
||||
|
||||
self.login_as_customer(
|
||||
"test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer"
|
||||
)
|
||||
create_address_and_contact(
|
||||
address_title="_Test Address for Customer 2",
|
||||
first_name="_Test Contact for Customer 2",
|
||||
email="test_contact_two_customer@example.com",
|
||||
customer="_Test Customer 2",
|
||||
)
|
||||
# clear existing quotations
|
||||
self.clear_existing_quotations()
|
||||
|
||||
# add first item
|
||||
update_cart("_Test Item", 1)
|
||||
|
||||
quotation = self.test_get_cart_customer()
|
||||
quotation = self.test_get_cart_customer("_Test Customer 2")
|
||||
|
||||
self.assertEqual(quotation.get("items")[0].item_code, "_Test Item")
|
||||
self.assertEqual(quotation.get("items")[0].qty, 1)
|
||||
@@ -95,7 +102,7 @@ class TestShoppingCart(unittest.TestCase):
|
||||
|
||||
# add second item
|
||||
update_cart("_Test Item 2", 1)
|
||||
quotation = self.test_get_cart_customer()
|
||||
quotation = self.test_get_cart_customer("_Test Customer 2")
|
||||
self.assertEqual(quotation.get("items")[1].item_code, "_Test Item 2")
|
||||
self.assertEqual(quotation.get("items")[1].qty, 1)
|
||||
self.assertEqual(quotation.get("items")[1].amount, 20)
|
||||
@@ -108,7 +115,7 @@ class TestShoppingCart(unittest.TestCase):
|
||||
|
||||
# update first item
|
||||
update_cart("_Test Item", 5)
|
||||
quotation = self.test_get_cart_customer()
|
||||
quotation = self.test_get_cart_customer("_Test Customer 2")
|
||||
self.assertEqual(quotation.get("items")[0].item_code, "_Test Item")
|
||||
self.assertEqual(quotation.get("items")[0].qty, 5)
|
||||
self.assertEqual(quotation.get("items")[0].amount, 50)
|
||||
@@ -121,7 +128,7 @@ class TestShoppingCart(unittest.TestCase):
|
||||
|
||||
# remove first item
|
||||
update_cart("_Test Item", 0)
|
||||
quotation = self.test_get_cart_customer()
|
||||
quotation = self.test_get_cart_customer("_Test Customer 2")
|
||||
|
||||
self.assertEqual(quotation.get("items")[0].item_code, "_Test Item 2")
|
||||
self.assertEqual(quotation.get("items")[0].qty, 1)
|
||||
@@ -129,9 +136,20 @@ class TestShoppingCart(unittest.TestCase):
|
||||
self.assertEqual(quotation.net_total, 20)
|
||||
self.assertEqual(len(quotation.get("items")), 1)
|
||||
|
||||
@unittest.skip("Flaky in CI")
|
||||
def test_tax_rule(self):
|
||||
self.create_tax_rule()
|
||||
self.login_as_customer()
|
||||
|
||||
self.login_as_customer(
|
||||
"test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer"
|
||||
)
|
||||
create_address_and_contact(
|
||||
address_title="_Test Address for Customer 2",
|
||||
first_name="_Test Contact for Customer 2",
|
||||
email="test_contact_two_customer@example.com",
|
||||
customer="_Test Customer 2",
|
||||
)
|
||||
|
||||
quotation = self.create_quotation()
|
||||
|
||||
from erpnext.accounts.party import set_taxes
|
||||
@@ -319,7 +337,7 @@ class TestShoppingCart(unittest.TestCase):
|
||||
if frappe.db.exists("User", email):
|
||||
return
|
||||
|
||||
frappe.get_doc(
|
||||
user = frappe.get_doc(
|
||||
{
|
||||
"doctype": "User",
|
||||
"user_type": "Website User",
|
||||
@@ -329,6 +347,40 @@ class TestShoppingCart(unittest.TestCase):
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
user.add_roles("Customer")
|
||||
|
||||
|
||||
def create_address_and_contact(**kwargs):
|
||||
if not frappe.db.get_value("Address", {"address_title": kwargs.get("address_title")}):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Address",
|
||||
"address_title": kwargs.get("address_title"),
|
||||
"address_type": kwargs.get("address_type") or "Office",
|
||||
"address_line1": kwargs.get("address_line1") or "Station Road",
|
||||
"city": kwargs.get("city") or "_Test City",
|
||||
"state": kwargs.get("state") or "Test State",
|
||||
"country": kwargs.get("country") or "India",
|
||||
"links": [
|
||||
{"link_doctype": "Customer", "link_name": kwargs.get("customer") or "_Test Customer"}
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
if not frappe.db.get_value("Contact", {"first_name": kwargs.get("first_name")}):
|
||||
contact = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Contact",
|
||||
"first_name": kwargs.get("first_name"),
|
||||
"links": [
|
||||
{"link_doctype": "Customer", "link_name": kwargs.get("customer") or "_Test Customer"}
|
||||
],
|
||||
}
|
||||
)
|
||||
contact.add_email(kwargs.get("email") or "test_contact_customer@example.com", is_primary=True)
|
||||
contact.add_phone(kwargs.get("phone") or "+91 0000000000", is_primary_phone=True)
|
||||
contact.insert()
|
||||
|
||||
|
||||
test_dependencies = [
|
||||
"Sales Taxes and Charges Template",
|
||||
|
||||
@@ -296,18 +296,27 @@ class LeaveAllocation(Document):
|
||||
|
||||
def get_previous_allocation(from_date, leave_type, employee):
|
||||
"""Returns document properties of previous allocation"""
|
||||
return frappe.db.get_value(
|
||||
"Leave Allocation",
|
||||
filters={
|
||||
"to_date": ("<", from_date),
|
||||
"leave_type": leave_type,
|
||||
"employee": employee,
|
||||
"docstatus": 1,
|
||||
},
|
||||
order_by="to_date DESC",
|
||||
fieldname=["name", "from_date", "to_date", "employee", "leave_type"],
|
||||
as_dict=1,
|
||||
)
|
||||
Allocation = frappe.qb.DocType("Leave Allocation")
|
||||
allocations = (
|
||||
frappe.qb.from_(Allocation)
|
||||
.select(
|
||||
Allocation.name,
|
||||
Allocation.from_date,
|
||||
Allocation.to_date,
|
||||
Allocation.employee,
|
||||
Allocation.leave_type,
|
||||
)
|
||||
.where(
|
||||
(Allocation.employee == employee)
|
||||
& (Allocation.leave_type == leave_type)
|
||||
& (Allocation.to_date < from_date)
|
||||
& (Allocation.docstatus == 1)
|
||||
)
|
||||
.orderby(Allocation.to_date, order=frappe.qb.desc)
|
||||
.limit(1)
|
||||
).run(as_dict=True)
|
||||
|
||||
return allocations[0] if allocations else None
|
||||
|
||||
|
||||
def get_leave_allocation_for_period(
|
||||
|
||||
@@ -706,19 +706,22 @@ def get_allocation_expiry_for_cf_leaves(
|
||||
employee: str, leave_type: str, to_date: str, from_date: str
|
||||
) -> str:
|
||||
"""Returns expiry of carry forward allocation in leave ledger entry"""
|
||||
expiry = frappe.get_all(
|
||||
"Leave Ledger Entry",
|
||||
filters={
|
||||
"employee": employee,
|
||||
"leave_type": leave_type,
|
||||
"is_carry_forward": 1,
|
||||
"transaction_type": "Leave Allocation",
|
||||
"to_date": ["between", (from_date, to_date)],
|
||||
"docstatus": 1,
|
||||
},
|
||||
fields=["to_date"],
|
||||
)
|
||||
return expiry[0]["to_date"] if expiry else ""
|
||||
Ledger = frappe.qb.DocType("Leave Ledger Entry")
|
||||
expiry = (
|
||||
frappe.qb.from_(Ledger)
|
||||
.select(Ledger.to_date)
|
||||
.where(
|
||||
(Ledger.employee == employee)
|
||||
& (Ledger.leave_type == leave_type)
|
||||
& (Ledger.is_carry_forward == 1)
|
||||
& (Ledger.transaction_type == "Leave Allocation")
|
||||
& (Ledger.to_date.between(from_date, to_date))
|
||||
& (Ledger.docstatus == 1)
|
||||
)
|
||||
.limit(1)
|
||||
).run()
|
||||
|
||||
return expiry[0][0] if expiry else ""
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -1017,7 +1020,7 @@ def get_leaves_for_period(
|
||||
if leave_entry.leaves % 1:
|
||||
half_day = 1
|
||||
half_day_date = frappe.db.get_value(
|
||||
"Leave Application", {"name": leave_entry.transaction_name}, ["half_day_date"]
|
||||
"Leave Application", leave_entry.transaction_name, "half_day_date"
|
||||
)
|
||||
|
||||
leave_days += (
|
||||
|
||||
@@ -713,25 +713,31 @@ class TestLeaveApplication(unittest.TestCase):
|
||||
self.assertEqual(details.leave_balance, 30)
|
||||
|
||||
def test_earned_leaves_creation(self):
|
||||
from erpnext.hr.utils import allocate_earned_leaves
|
||||
from erpnext.hr.doctype.leave_policy_assignment.test_leave_policy_assignment import (
|
||||
allocate_earned_leaves_for_months,
|
||||
)
|
||||
|
||||
leave_period = get_leave_period()
|
||||
year_start = get_year_start(getdate())
|
||||
year_end = get_year_ending(getdate())
|
||||
frappe.flags.current_date = year_start
|
||||
|
||||
leave_period = get_leave_period(year_start, year_end)
|
||||
employee = get_employee()
|
||||
leave_type = "Test Earned Leave Type"
|
||||
|
||||
make_policy_assignment(employee, leave_type, leave_period)
|
||||
|
||||
for i in range(0, 14):
|
||||
allocate_earned_leaves()
|
||||
|
||||
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 6)
|
||||
# leaves for 6 months = 3, but max leaves restricts allocation to 2
|
||||
frappe.db.set_value("Leave Type", leave_type, "max_leaves_allowed", 2)
|
||||
allocate_earned_leaves_for_months(6)
|
||||
self.assertEqual(get_leave_balance_on(employee.name, leave_type, frappe.flags.current_date), 2)
|
||||
|
||||
# validate earned leaves creation without maximum leaves
|
||||
frappe.db.set_value("Leave Type", leave_type, "max_leaves_allowed", 0)
|
||||
allocate_earned_leaves_for_months(5)
|
||||
self.assertEqual(get_leave_balance_on(employee.name, leave_type, frappe.flags.current_date), 4.5)
|
||||
|
||||
for i in range(0, 6):
|
||||
allocate_earned_leaves()
|
||||
|
||||
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9)
|
||||
frappe.flags.current_date = None
|
||||
|
||||
# test to not consider current leave in leave balance while submitting
|
||||
def test_current_leave_on_submit(self):
|
||||
@@ -1254,7 +1260,7 @@ def set_leave_approver():
|
||||
dept_doc.save(ignore_permissions=True)
|
||||
|
||||
|
||||
def get_leave_period():
|
||||
def get_leave_period(from_date=None, to_date=None):
|
||||
leave_period_name = frappe.db.exists({"doctype": "Leave Period", "company": "_Test Company"})
|
||||
if leave_period_name:
|
||||
return frappe.get_doc("Leave Period", leave_period_name[0][0])
|
||||
@@ -1263,8 +1269,8 @@ def get_leave_period():
|
||||
dict(
|
||||
name="Test Leave Period",
|
||||
doctype="Leave Period",
|
||||
from_date=add_months(nowdate(), -6),
|
||||
to_date=add_months(nowdate(), 6),
|
||||
from_date=from_date or add_months(nowdate(), -6),
|
||||
to_date=to_date or add_months(nowdate(), 6),
|
||||
company="_Test Company",
|
||||
is_active=1,
|
||||
)
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Employee",
|
||||
"options": "Employee"
|
||||
"options": "Employee",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "employee.employee_name",
|
||||
@@ -57,13 +58,15 @@
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Transaction Type",
|
||||
"options": "DocType"
|
||||
"options": "DocType",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "transaction_name",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": "Transaction Name",
|
||||
"options": "transaction_type"
|
||||
"options": "transaction_type",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "leaves",
|
||||
@@ -123,7 +126,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-01-04 18:47:45.146652",
|
||||
"modified": "2023-11-17 12:36:36.963697",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Leave Ledger Entry",
|
||||
|
||||
@@ -225,3 +225,7 @@ def expire_carried_forward_allocation(allocation):
|
||||
to_date=allocation.to_date,
|
||||
)
|
||||
create_leave_ledger_entry(allocation, args)
|
||||
|
||||
|
||||
def on_doctype_update():
|
||||
frappe.db.add_index("Leave Ledger Entry", ["transaction_type", "transaction_name"])
|
||||
|
||||
@@ -100,7 +100,7 @@ class LeavePolicyAssignment(Document):
|
||||
return leave_allocations
|
||||
|
||||
def create_leave_allocation(
|
||||
self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining
|
||||
self, leave_type, annual_allocation, leave_type_details, date_of_joining
|
||||
):
|
||||
# Creates leave allocation for the given employee in the provided leave period
|
||||
carry_forward = self.carry_forward
|
||||
@@ -108,7 +108,7 @@ class LeavePolicyAssignment(Document):
|
||||
carry_forward = 0
|
||||
|
||||
new_leaves_allocated = self.get_new_leaves(
|
||||
leave_type, new_leaves_allocated, leave_type_details, date_of_joining
|
||||
leave_type, annual_allocation, leave_type_details, date_of_joining
|
||||
)
|
||||
|
||||
allocation = frappe.get_doc(
|
||||
@@ -129,7 +129,7 @@ class LeavePolicyAssignment(Document):
|
||||
allocation.submit()
|
||||
return allocation.name, new_leaves_allocated
|
||||
|
||||
def get_new_leaves(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining):
|
||||
def get_new_leaves(self, leave_type, annual_allocation, leave_type_details, date_of_joining):
|
||||
from frappe.model.meta import get_field_precision
|
||||
|
||||
precision = get_field_precision(
|
||||
@@ -146,20 +146,27 @@ class LeavePolicyAssignment(Document):
|
||||
else:
|
||||
# get leaves for past months if assignment is based on Leave Period / Joining Date
|
||||
new_leaves_allocated = self.get_leaves_for_passed_months(
|
||||
leave_type, new_leaves_allocated, leave_type_details, date_of_joining
|
||||
leave_type, annual_allocation, leave_type_details, date_of_joining
|
||||
)
|
||||
|
||||
# Calculate leaves at pro-rata basis for employees joining after the beginning of the given leave period
|
||||
elif getdate(date_of_joining) > getdate(self.effective_from):
|
||||
remaining_period = (date_diff(self.effective_to, date_of_joining) + 1) / (
|
||||
date_diff(self.effective_to, self.effective_from) + 1
|
||||
)
|
||||
new_leaves_allocated = ceil(new_leaves_allocated * remaining_period)
|
||||
else:
|
||||
if getdate(date_of_joining) > getdate(self.effective_from):
|
||||
remaining_period = (date_diff(self.effective_to, date_of_joining) + 1) / (
|
||||
date_diff(self.effective_to, self.effective_from) + 1
|
||||
)
|
||||
new_leaves_allocated = ceil(annual_allocation * remaining_period)
|
||||
else:
|
||||
new_leaves_allocated = annual_allocation
|
||||
|
||||
# leave allocation should not exceed annual allocation as per policy assignment
|
||||
if new_leaves_allocated > annual_allocation:
|
||||
new_leaves_allocated = annual_allocation
|
||||
|
||||
return flt(new_leaves_allocated, precision)
|
||||
|
||||
def get_leaves_for_passed_months(
|
||||
self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining
|
||||
self, leave_type, annual_allocation, leave_type_details, date_of_joining
|
||||
):
|
||||
from erpnext.hr.utils import get_monthly_earned_leave
|
||||
|
||||
@@ -184,7 +191,7 @@ class LeavePolicyAssignment(Document):
|
||||
|
||||
if months_passed > 0:
|
||||
monthly_earned_leave = get_monthly_earned_leave(
|
||||
new_leaves_allocated,
|
||||
annual_allocation,
|
||||
leave_type_details.get(leave_type).earned_leave_frequency,
|
||||
leave_type_details.get(leave_type).rounding,
|
||||
)
|
||||
|
||||
@@ -5,8 +5,10 @@ import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, add_months, get_first_day, get_last_day, getdate
|
||||
from frappe.utils import add_days, add_months, get_first_day, get_last_day, get_year_start, getdate
|
||||
|
||||
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
|
||||
from erpnext.hr.doctype.leave_application.leave_application import get_leave_balance_on
|
||||
from erpnext.hr.doctype.leave_application.test_leave_application import (
|
||||
get_employee,
|
||||
get_leave_period,
|
||||
@@ -15,6 +17,7 @@ from erpnext.hr.doctype.leave_policy.test_leave_policy import create_leave_polic
|
||||
from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
|
||||
create_assignment_for_multiple_employees,
|
||||
)
|
||||
from erpnext.hr.utils import allocate_earned_leaves
|
||||
|
||||
test_dependencies = ["Employee"]
|
||||
|
||||
@@ -34,6 +37,8 @@ class TestLeavePolicyAssignment(FrappeTestCase):
|
||||
self.original_doj = employee.date_of_joining
|
||||
self.employee = employee
|
||||
|
||||
self.leave_type = "Test Earned Leave"
|
||||
|
||||
def test_grant_leaves(self):
|
||||
leave_period = get_leave_period()
|
||||
# allocation = 10
|
||||
@@ -326,6 +331,90 @@ class TestLeavePolicyAssignment(FrappeTestCase):
|
||||
self.assertEqual(effective_from, self.employee.date_of_joining)
|
||||
self.assertEqual(leaves_allocated, 3)
|
||||
|
||||
def test_overallocation(self):
|
||||
"""Tests if earned leave allocation does not exceed annual allocation"""
|
||||
frappe.flags.current_date = get_year_start(getdate())
|
||||
make_policy_assignment(
|
||||
self.employee,
|
||||
annual_allocation=22,
|
||||
allocate_on_day="First Day",
|
||||
start_date=frappe.flags.current_date,
|
||||
)
|
||||
|
||||
# leaves for 12 months = 22
|
||||
# With rounding, 22 leaves would be allocated in 11 months only
|
||||
frappe.db.set_value("Leave Type", self.leave_type, "rounding", 1.0)
|
||||
allocate_earned_leaves_for_months(11)
|
||||
self.assertEqual(
|
||||
get_leave_balance_on(self.employee.name, self.leave_type, frappe.flags.current_date), 22
|
||||
)
|
||||
|
||||
# should not allocate more leaves than annual allocation
|
||||
allocate_earned_leaves_for_months(1)
|
||||
self.assertEqual(
|
||||
get_leave_balance_on(self.employee.name, self.leave_type, frappe.flags.current_date), 22
|
||||
)
|
||||
|
||||
def test_over_allocation_during_assignment_creation(self):
|
||||
"""Tests backdated earned leave allocation does not exceed annual allocation"""
|
||||
start_date = get_first_day(add_months(getdate(), -12))
|
||||
|
||||
# joining date set to 1Y ago
|
||||
self.employee.date_of_joining = start_date
|
||||
self.employee.save()
|
||||
|
||||
# create backdated assignment for last year
|
||||
frappe.flags.current_date = get_first_day(getdate())
|
||||
|
||||
leave_policy_assignments = make_policy_assignment(
|
||||
self.employee, start_date=start_date, allocate_on_day="Date of Joining"
|
||||
)
|
||||
|
||||
# 13 months have passed but annual allocation = 12
|
||||
# check annual allocation is not exceeded
|
||||
leaves_allocated = get_allocated_leaves(leave_policy_assignments[0])
|
||||
self.assertEqual(leaves_allocated, 12)
|
||||
|
||||
def test_overallocation_with_carry_forwarding(self):
|
||||
"""Tests earned leave allocation with cf leaves does not exceed annual allocation"""
|
||||
year_start = get_year_start(getdate())
|
||||
|
||||
# initial leave allocation = 5
|
||||
leave_allocation = create_leave_allocation(
|
||||
employee=self.employee.name,
|
||||
employee_name=self.employee.employee_name,
|
||||
leave_type=self.leave_type,
|
||||
from_date=get_first_day(add_months(year_start, -1)),
|
||||
to_date=get_last_day(add_months(year_start, -1)),
|
||||
new_leaves_allocated=5,
|
||||
carry_forward=0,
|
||||
)
|
||||
leave_allocation.submit()
|
||||
|
||||
frappe.flags.current_date = year_start
|
||||
# carry forwarded leaves = 5
|
||||
make_policy_assignment(
|
||||
self.employee,
|
||||
annual_allocation=22,
|
||||
allocate_on_day="First Day",
|
||||
start_date=year_start,
|
||||
carry_forward=True,
|
||||
)
|
||||
|
||||
frappe.db.set_value("Leave Type", self.leave_type, "rounding", 1.0)
|
||||
allocate_earned_leaves_for_months(11)
|
||||
|
||||
# 5 carry forwarded leaves + 22 EL allocated = 27 leaves
|
||||
self.assertEqual(
|
||||
get_leave_balance_on(self.employee.name, self.leave_type, frappe.flags.current_date), 27
|
||||
)
|
||||
|
||||
# should not allocate more leaves than annual allocation (22 excluding 5 cf leaves)
|
||||
allocate_earned_leaves_for_months(1)
|
||||
self.assertEqual(
|
||||
get_leave_balance_on(self.employee.name, self.leave_type, frappe.flags.current_date), 27
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.set_value("Employee", self.employee.name, "date_of_joining", self.original_doj)
|
||||
frappe.flags.current_date = None
|
||||
@@ -376,3 +465,51 @@ def setup_leave_period_and_policy(start_date, based_on_doj=False):
|
||||
).insert()
|
||||
|
||||
return leave_period, leave_policy
|
||||
|
||||
|
||||
def make_policy_assignment(
|
||||
employee,
|
||||
allocate_on_day="Last Day",
|
||||
earned_leave_frequency="Monthly",
|
||||
start_date=None,
|
||||
annual_allocation=12,
|
||||
carry_forward=0,
|
||||
assignment_based_on="Leave Period",
|
||||
):
|
||||
leave_type = create_earned_leave_type("Test Earned Leave", allocate_on_day)
|
||||
leave_period = create_leave_period("Test Earned Leave Period", start_date=start_date)
|
||||
leave_policy = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Leave Policy",
|
||||
"title": "Test Earned Leave Policy",
|
||||
"leave_policy_details": [
|
||||
{"leave_type": leave_type.name, "annual_allocation": annual_allocation}
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
data = {
|
||||
"assignment_based_on": assignment_based_on,
|
||||
"leave_policy": leave_policy.name,
|
||||
"leave_period": leave_period.name,
|
||||
"carry_forward": carry_forward,
|
||||
}
|
||||
|
||||
leave_policy_assignments = create_assignment_for_multiple_employees(
|
||||
[employee.name], frappe._dict(data)
|
||||
)
|
||||
return leave_policy_assignments
|
||||
|
||||
|
||||
def get_allocated_leaves(assignment):
|
||||
return frappe.db.get_value(
|
||||
"Leave Allocation",
|
||||
{"leave_policy_assignment": assignment},
|
||||
"total_leaves_allocated",
|
||||
)
|
||||
|
||||
|
||||
def allocate_earned_leaves_for_months(months):
|
||||
for i in range(0, months):
|
||||
frappe.flags.current_date = add_months(frappe.flags.current_date, 1)
|
||||
allocate_earned_leaves()
|
||||
|
||||
@@ -1,26 +1,32 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"apply_user_permissions": 1,
|
||||
"creation": "2013-02-22 15:29:34",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2017-02-24 20:18:04.317397",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Employee Leave Balance",
|
||||
"owner": "Administrator",
|
||||
"ref_doctype": "Employee",
|
||||
"report_name": "Employee Leave Balance",
|
||||
"report_type": "Script Report",
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2013-02-22 15:29:34",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 3,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2023-11-17 13:28:40.669200",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Employee Leave Balance",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Employee",
|
||||
"report_name": "Employee Leave Balance",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "HR User"
|
||||
},
|
||||
},
|
||||
{
|
||||
"role": "HR Manager"
|
||||
},
|
||||
{
|
||||
"role": "Employee"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -85,19 +85,10 @@ def get_columns() -> List[Dict]:
|
||||
|
||||
|
||||
def get_data(filters: Filters) -> List:
|
||||
leave_types = frappe.db.get_list("Leave Type", pluck="name", order_by="name")
|
||||
conditions = get_conditions(filters)
|
||||
leave_types = get_leave_types()
|
||||
active_employees = get_employees(filters)
|
||||
|
||||
user = frappe.session.user
|
||||
department_approver_map = get_department_leave_approver_map(filters.department)
|
||||
|
||||
active_employees = frappe.get_list(
|
||||
"Employee",
|
||||
filters=conditions,
|
||||
fields=["name", "employee_name", "department", "user_id", "leave_approver"],
|
||||
)
|
||||
|
||||
precision = cint(frappe.db.get_single_value("System Settings", "float_precision", cache=True))
|
||||
precision = cint(frappe.db.get_single_value("System Settings", "float_precision"))
|
||||
consolidate_leave_types = len(active_employees) > 1 and filters.consolidate_leave_types
|
||||
row = None
|
||||
|
||||
@@ -110,10 +101,6 @@ def get_data(filters: Filters) -> List:
|
||||
row = frappe._dict({"leave_type": leave_type})
|
||||
|
||||
for employee in active_employees:
|
||||
leave_approvers = department_approver_map.get(employee.department_name, []).append(
|
||||
employee.leave_approver
|
||||
)
|
||||
|
||||
if consolidate_leave_types:
|
||||
row = frappe._dict()
|
||||
else:
|
||||
@@ -144,6 +131,35 @@ def get_data(filters: Filters) -> List:
|
||||
return data
|
||||
|
||||
|
||||
def get_leave_types() -> List[str]:
|
||||
LeaveType = frappe.qb.DocType("Leave Type")
|
||||
leave_types = (frappe.qb.from_(LeaveType).select(LeaveType.name).orderby(LeaveType.name)).run(
|
||||
as_dict=True
|
||||
)
|
||||
return [leave_type.name for leave_type in leave_types]
|
||||
|
||||
|
||||
def get_employees(filters: Filters) -> List[Dict]:
|
||||
Employee = frappe.qb.DocType("Employee")
|
||||
query = frappe.qb.from_(Employee).select(
|
||||
Employee.name,
|
||||
Employee.employee_name,
|
||||
Employee.department,
|
||||
)
|
||||
|
||||
for field in ["company", "department"]:
|
||||
if filters.get(field):
|
||||
query = query.where((getattr(Employee, field) == filters.get(field)))
|
||||
|
||||
if filters.get("employee"):
|
||||
query = query.where(Employee.name == filters.get("employee"))
|
||||
|
||||
if filters.get("employee_status"):
|
||||
query = query.where(Employee.status == filters.get("employee_status"))
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_opening_balance(
|
||||
employee: str, leave_type: str, filters: Filters, carry_forwarded_leaves: float
|
||||
) -> float:
|
||||
@@ -168,48 +184,6 @@ def get_opening_balance(
|
||||
return opening_balance
|
||||
|
||||
|
||||
def get_conditions(filters: Filters) -> Dict:
|
||||
conditions = {}
|
||||
|
||||
if filters.employee:
|
||||
conditions["name"] = filters.employee
|
||||
|
||||
if filters.company:
|
||||
conditions["company"] = filters.company
|
||||
|
||||
if filters.department:
|
||||
conditions["department"] = filters.department
|
||||
|
||||
if filters.employee_status:
|
||||
conditions["status"] = filters.employee_status
|
||||
|
||||
return conditions
|
||||
|
||||
|
||||
def get_department_leave_approver_map(department: Optional[str] = None):
|
||||
# get current department and all its child
|
||||
department_list = frappe.get_list(
|
||||
"Department",
|
||||
filters={"disabled": 0},
|
||||
or_filters={"name": department, "parent_department": department},
|
||||
pluck="name",
|
||||
)
|
||||
# retrieve approvers list from current department and from its subsequent child departments
|
||||
approver_list = frappe.get_all(
|
||||
"Department Approver",
|
||||
filters={"parentfield": "leave_approvers", "parent": ("in", department_list)},
|
||||
fields=["parent", "approver"],
|
||||
as_list=True,
|
||||
)
|
||||
|
||||
approvers = {}
|
||||
|
||||
for k, v in approver_list:
|
||||
approvers.setdefault(k, []).append(v)
|
||||
|
||||
return approvers
|
||||
|
||||
|
||||
def get_allocated_and_expired_leaves(
|
||||
from_date: str, to_date: str, employee: str, leave_type: str
|
||||
) -> Tuple[float, float, float]:
|
||||
@@ -244,7 +218,7 @@ def get_leave_ledger_entries(
|
||||
from_date: str, to_date: str, employee: str, leave_type: str
|
||||
) -> List[Dict]:
|
||||
ledger = frappe.qb.DocType("Leave Ledger Entry")
|
||||
records = (
|
||||
return (
|
||||
frappe.qb.from_(ledger)
|
||||
.select(
|
||||
ledger.employee,
|
||||
@@ -270,8 +244,6 @@ def get_leave_ledger_entries(
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
return records
|
||||
|
||||
|
||||
def get_chart_data(data: List, filters: Filters) -> Dict:
|
||||
labels = []
|
||||
|
||||
@@ -6,9 +6,6 @@ import frappe
|
||||
from frappe import _
|
||||
|
||||
from erpnext.hr.doctype.leave_application.leave_application import get_leave_details
|
||||
from erpnext.hr.report.employee_leave_balance.employee_leave_balance import (
|
||||
get_department_leave_approver_map,
|
||||
)
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -54,17 +51,11 @@ def get_data(filters, leave_types):
|
||||
active_employees = frappe.get_all(
|
||||
"Employee",
|
||||
filters=conditions,
|
||||
fields=["name", "employee_name", "department", "user_id", "leave_approver"],
|
||||
fields=["name", "employee_name", "department", "user_id"],
|
||||
)
|
||||
|
||||
department_approver_map = get_department_leave_approver_map(filters.get("department"))
|
||||
|
||||
data = []
|
||||
for employee in active_employees:
|
||||
leave_approvers = department_approver_map.get(employee.department_name, [])
|
||||
if employee.leave_approver:
|
||||
leave_approvers.append(employee.leave_approver)
|
||||
|
||||
row = [employee.name, employee.employee_name, employee.department]
|
||||
available_leave = get_leave_details(employee.name, filters.date)
|
||||
for leave_type in leave_types:
|
||||
|
||||
@@ -459,7 +459,7 @@ def generate_leave_encashment():
|
||||
def allocate_earned_leaves():
|
||||
"""Allocate earned leaves to Employees"""
|
||||
e_leave_types = get_earned_leaves()
|
||||
today = getdate()
|
||||
today = frappe.flags.current_date or getdate()
|
||||
|
||||
for e_leave_type in e_leave_types:
|
||||
|
||||
@@ -496,18 +496,28 @@ def allocate_earned_leaves():
|
||||
|
||||
|
||||
def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type):
|
||||
allocation = frappe.get_doc("Leave Allocation", allocation.name)
|
||||
annual_allocation = flt(annual_allocation, allocation.precision("total_leaves_allocated"))
|
||||
|
||||
earned_leaves = get_monthly_earned_leave(
|
||||
annual_allocation, e_leave_type.earned_leave_frequency, e_leave_type.rounding
|
||||
)
|
||||
|
||||
allocation = frappe.get_doc("Leave Allocation", allocation.name)
|
||||
new_allocation = flt(allocation.total_leaves_allocated) + flt(earned_leaves)
|
||||
new_allocation_without_cf = flt(
|
||||
flt(allocation.get_existing_leave_count()) + flt(earned_leaves),
|
||||
allocation.precision("total_leaves_allocated"),
|
||||
)
|
||||
|
||||
if new_allocation > e_leave_type.max_leaves_allowed and e_leave_type.max_leaves_allowed > 0:
|
||||
new_allocation = e_leave_type.max_leaves_allowed
|
||||
|
||||
if new_allocation != allocation.total_leaves_allocated:
|
||||
today_date = today()
|
||||
if (
|
||||
new_allocation != allocation.total_leaves_allocated
|
||||
# annual allocation as per policy should not be exceeded
|
||||
and new_allocation_without_cf <= annual_allocation
|
||||
):
|
||||
today_date = frappe.flags.current_date or getdate()
|
||||
|
||||
allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False)
|
||||
create_additional_leave_ledger_entry(allocation, earned_leaves, today_date)
|
||||
|
||||
@@ -306,7 +306,7 @@ def get_last_accrual_date(loan, posting_date):
|
||||
def get_last_disbursement_date(loan, posting_date):
|
||||
last_disbursement_date = frappe.db.get_value(
|
||||
"Loan Disbursement",
|
||||
{"docstatus": 1, "against_loan": loan, "posting_date": ("<", posting_date)},
|
||||
{"docstatus": 1, "against_loan": loan, "posting_date": ("<=", posting_date)},
|
||||
"MAX(posting_date)",
|
||||
)
|
||||
|
||||
|
||||
@@ -378,3 +378,4 @@ erpnext.patches.v13_0.update_schedule_type_in_loans
|
||||
erpnext.patches.v13_0.update_asset_value_for_manual_depr_entries
|
||||
erpnext.patches.v13_0.update_docs_link
|
||||
erpnext.patches.v13_0.correct_asset_value_if_je_with_workflow
|
||||
execute:frappe.db.set_value("Accounts Settings", "Accounts Settings", "service_provider", "frankfurter.app")
|
||||
|
||||
@@ -3,23 +3,27 @@ import frappe
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc("stock", "doctype", "quality_inspection_parameter")
|
||||
params = set()
|
||||
|
||||
# get all distinct parameters from QI readigs table
|
||||
reading_params = frappe.db.get_all(
|
||||
"Quality Inspection Reading", fields=["distinct specification"]
|
||||
)
|
||||
reading_params = [d.specification for d in reading_params]
|
||||
# get all parameters from QI readings table
|
||||
for (p,) in frappe.db.get_all(
|
||||
"Quality Inspection Reading", fields=["specification"], as_list=True
|
||||
):
|
||||
params.add(p.strip())
|
||||
|
||||
# get all distinct parameters from QI Template as some may be unused in QI
|
||||
template_params = frappe.db.get_all(
|
||||
"Item Quality Inspection Parameter", fields=["distinct specification"]
|
||||
)
|
||||
template_params = [d.specification for d in template_params]
|
||||
# get all parameters from QI Template as some may be unused in QI
|
||||
for (p,) in frappe.db.get_all(
|
||||
"Item Quality Inspection Parameter", fields=["specification"], as_list=True
|
||||
):
|
||||
params.add(p.strip())
|
||||
|
||||
params = list(set(reading_params + template_params))
|
||||
# because db primary keys are case insensitive, so duplicates will cause an exception
|
||||
params = set({x.casefold(): x for x in params}.values())
|
||||
|
||||
for parameter in params:
|
||||
if not frappe.db.exists("Quality Inspection Parameter", parameter):
|
||||
frappe.get_doc(
|
||||
{"doctype": "Quality Inspection Parameter", "parameter": parameter, "description": parameter}
|
||||
).insert(ignore_permissions=True)
|
||||
if frappe.db.exists("Quality Inspection Parameter", parameter):
|
||||
continue
|
||||
|
||||
frappe.get_doc(
|
||||
{"doctype": "Quality Inspection Parameter", "parameter": parameter, "description": parameter}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
@@ -125,6 +125,7 @@ def execute():
|
||||
loan_type_doc.company = loan.company
|
||||
loan_type_doc.mode_of_payment = loan.mode_of_payment
|
||||
loan_type_doc.payment_account = loan.payment_account
|
||||
loan_type_doc.disbursement_account = loan.payment_account
|
||||
loan_type_doc.loan_account = loan.loan_account
|
||||
loan_type_doc.interest_income_account = loan.interest_income_account
|
||||
loan_type_doc.penalty_income_account = penalty_account
|
||||
|
||||
@@ -71,6 +71,12 @@ class Timesheet(Document):
|
||||
if args.is_billable:
|
||||
if flt(args.billing_hours) == 0.0:
|
||||
args.billing_hours = args.hours
|
||||
elif flt(args.billing_hours) > flt(args.hours):
|
||||
frappe.msgprint(
|
||||
_("Warning - Row {0}: Billing Hours are more than Actual Hours").format(args.idx),
|
||||
indicator="orange",
|
||||
alert=True,
|
||||
)
|
||||
else:
|
||||
args.billing_hours = 0
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ frappe.ui.form.on("Communication", {
|
||||
frappe.confirm(__(confirm_msg, [__("Issue")]), () => {
|
||||
frm.trigger('make_issue_from_communication');
|
||||
})
|
||||
}, "Create");
|
||||
}, __("Create"));
|
||||
}
|
||||
|
||||
if(!in_list(["Lead", "Opportunity"], frm.doc.reference_doctype)) {
|
||||
|
||||
@@ -1378,7 +1378,7 @@ class GSPConnector:
|
||||
|
||||
def set_einvoice_data(self, res):
|
||||
enc_signed_invoice = res.get("SignedInvoice")
|
||||
dec_signed_invoice = jwt.decode(enc_signed_invoice, verify=False)["data"]
|
||||
dec_signed_invoice = jwt.decode(enc_signed_invoice, options={"verify_signature": False})["data"]
|
||||
|
||||
self.invoice.irn = res.get("Irn")
|
||||
self.invoice.ewaybill = res.get("EwbNo")
|
||||
|
||||
@@ -532,6 +532,7 @@ def check_credit_limit(customer, company, ignore_outstanding_sales_order=False,
|
||||
primary_action={
|
||||
"label": "Send Email",
|
||||
"server_action": "erpnext.selling.doctype.customer.customer.send_emails",
|
||||
"hide_on_success": True,
|
||||
"args": {
|
||||
"customer": customer,
|
||||
"customer_outstanding": customer_outstanding,
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe import _, qb
|
||||
from frappe.query_builder import Criterion
|
||||
|
||||
from erpnext import get_default_company
|
||||
from erpnext.accounts.party import get_party_details
|
||||
from erpnext.stock.get_item_details import get_price_list_rate_for
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
@@ -50,6 +50,42 @@ def get_columns(filters=None):
|
||||
]
|
||||
|
||||
|
||||
def fetch_item_prices(
|
||||
customer: str = None, price_list: str = None, selling_price_list: str = None, items: list = None
|
||||
):
|
||||
price_list_map = frappe._dict()
|
||||
ip = qb.DocType("Item Price")
|
||||
and_conditions = []
|
||||
or_conditions = []
|
||||
if items:
|
||||
and_conditions.append(ip.item_code.isin([x.item_code for x in items]))
|
||||
and_conditions.append(ip.selling == True)
|
||||
|
||||
or_conditions.append(ip.customer == None)
|
||||
or_conditions.append(ip.price_list == None)
|
||||
|
||||
if customer:
|
||||
or_conditions.append(ip.customer == customer)
|
||||
|
||||
if price_list:
|
||||
or_conditions.append(ip.price_list == price_list)
|
||||
|
||||
if selling_price_list:
|
||||
or_conditions.append(ip.price_list == selling_price_list)
|
||||
|
||||
res = (
|
||||
qb.from_(ip)
|
||||
.select(ip.item_code, ip.price_list, ip.price_list_rate)
|
||||
.where(Criterion.all(and_conditions))
|
||||
.where(Criterion.any(or_conditions))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
for x in res:
|
||||
price_list_map.update({(x.item_code, x.price_list): x.price_list_rate})
|
||||
|
||||
return price_list_map
|
||||
|
||||
|
||||
def get_data(filters=None):
|
||||
data = []
|
||||
customer_details = get_customer_details(filters)
|
||||
@@ -59,9 +95,17 @@ def get_data(filters=None):
|
||||
"Bin", fields=["item_code", "sum(actual_qty) AS available"], group_by="item_code"
|
||||
)
|
||||
item_stock_map = {item.item_code: item.available for item in item_stock_map}
|
||||
price_list_map = fetch_item_prices(
|
||||
customer_details.customer,
|
||||
customer_details.price_list,
|
||||
customer_details.selling_price_list,
|
||||
items,
|
||||
)
|
||||
|
||||
for item in items:
|
||||
price_list_rate = get_price_list_rate_for(customer_details, item.item_code) or 0.0
|
||||
price_list_rate = price_list_map.get(
|
||||
(item.item_code, customer_details.price_list or customer_details.selling_price_list), 0.0
|
||||
)
|
||||
available_stock = item_stock_map.get(item.item_code)
|
||||
|
||||
data.append(
|
||||
|
||||
@@ -75,7 +75,7 @@ class TestCurrencyExchange(unittest.TestCase):
|
||||
self.clear_cache()
|
||||
exchange_rate = get_exchange_rate("USD", "INR", "2015-12-15", "for_selling")
|
||||
self.assertFalse(exchange_rate == 60)
|
||||
self.assertEqual(flt(exchange_rate, 3), 66.999)
|
||||
self.assertEqual(flt(exchange_rate, 3), 66.894)
|
||||
|
||||
def test_exchange_rate_strict(self):
|
||||
# strict currency settings
|
||||
@@ -87,7 +87,7 @@ class TestCurrencyExchange(unittest.TestCase):
|
||||
|
||||
self.clear_cache()
|
||||
exchange_rate = get_exchange_rate("USD", "INR", "2016-01-15", "for_buying")
|
||||
self.assertEqual(flt(exchange_rate, 3), 67.235)
|
||||
self.assertEqual(flt(exchange_rate, 3), 67.79)
|
||||
|
||||
exchange_rate = get_exchange_rate("USD", "INR", "2016-01-30", "for_selling")
|
||||
self.assertEqual(exchange_rate, 62.9)
|
||||
@@ -95,7 +95,7 @@ class TestCurrencyExchange(unittest.TestCase):
|
||||
# Exchange rate as on 15th Dec, 2015
|
||||
self.clear_cache()
|
||||
exchange_rate = get_exchange_rate("USD", "INR", "2015-12-15", "for_buying")
|
||||
self.assertEqual(flt(exchange_rate, 3), 66.999)
|
||||
self.assertEqual(flt(exchange_rate, 3), 66.894)
|
||||
|
||||
def test_exchange_rate_strict_switched(self):
|
||||
# Start with allow_stale is True
|
||||
@@ -108,4 +108,4 @@ class TestCurrencyExchange(unittest.TestCase):
|
||||
# Will fetch from fixer.io
|
||||
self.clear_cache()
|
||||
exchange_rate = get_exchange_rate("USD", "INR", "2016-01-15", "for_buying")
|
||||
self.assertEqual(flt(exchange_rate, 3), 67.235)
|
||||
self.assertEqual(flt(exchange_rate, 3), 67.79)
|
||||
|
||||
@@ -33,6 +33,7 @@ def after_install():
|
||||
add_standard_navbar_items()
|
||||
add_app_name()
|
||||
add_non_standard_user_types()
|
||||
update_roles()
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
@@ -237,6 +238,12 @@ def create_custom_role(data):
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
|
||||
def update_roles():
|
||||
website_user_roles = ("Customer", "Supplier")
|
||||
for role in website_user_roles:
|
||||
frappe.db.set_value("Role", role, "desk_access", 0)
|
||||
|
||||
|
||||
def create_user_type(user_type, data):
|
||||
if frappe.db.exists("User Type", user_type):
|
||||
doc = frappe.get_cached_doc("User Type", user_type)
|
||||
|
||||
@@ -113,13 +113,30 @@ def get_exchange_rate(from_currency, to_currency, transaction_date=None, args=No
|
||||
if not value:
|
||||
import requests
|
||||
|
||||
api_url = "https://api.exchangerate.host/convert"
|
||||
response = requests.get(
|
||||
api_url, params={"date": transaction_date, "from": from_currency, "to": to_currency}
|
||||
)
|
||||
if currency_settings.service_provider == "exchangerate.host":
|
||||
api_url = "https://api.exchangerate.host/convert"
|
||||
response = requests.get(
|
||||
api_url,
|
||||
params={
|
||||
"access_key": currency_settings.access_key,
|
||||
"transaction_date": transaction_date,
|
||||
"amount": 1,
|
||||
"from": from_currency,
|
||||
"to": to_currency,
|
||||
},
|
||||
)
|
||||
# exchangerate.host return 200 for all requests. Can't rely on it to raise exception
|
||||
value = response.json()["result"]
|
||||
if not response.json()["success"]:
|
||||
raise frappe.ValidationError
|
||||
|
||||
else:
|
||||
api_url = f"https://api.frankfurter.app/{transaction_date}"
|
||||
response = requests.get(api_url, params={"from": from_currency, "to": to_currency})
|
||||
value = response.json()["rates"][to_currency]
|
||||
|
||||
# expire in 6 hours
|
||||
response.raise_for_status()
|
||||
value = response.json()["result"]
|
||||
cache.setex(name=key, time=21600, value=flt(value))
|
||||
return flt(value)
|
||||
except Exception:
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import frappe
|
||||
from frappe.utils import cint
|
||||
|
||||
|
||||
def get_leaderboards():
|
||||
@@ -54,12 +53,13 @@ def get_leaderboards():
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_all_customers(date_range, company, field, limit=None):
|
||||
filters = [["docstatus", "=", "1"], ["company", "=", company]]
|
||||
from_date, to_date = parse_date_range(date_range)
|
||||
if field == "outstanding_amount":
|
||||
filters = [["docstatus", "=", "1"], ["company", "=", company]]
|
||||
if date_range:
|
||||
date_range = frappe.parse_json(date_range)
|
||||
filters.append(["posting_date", ">=", "between", [date_range[0], date_range[1]]])
|
||||
return frappe.db.get_all(
|
||||
if from_date and to_date:
|
||||
filters.append(["posting_date", "between", [from_date, to_date]])
|
||||
|
||||
return frappe.get_list(
|
||||
"Sales Invoice",
|
||||
fields=["customer as name", "sum(outstanding_amount) as value"],
|
||||
filters=filters,
|
||||
@@ -69,26 +69,20 @@ def get_all_customers(date_range, company, field, limit=None):
|
||||
)
|
||||
else:
|
||||
if field == "total_sales_amount":
|
||||
select_field = "sum(so_item.base_net_amount)"
|
||||
select_field = "base_net_total"
|
||||
elif field == "total_qty_sold":
|
||||
select_field = "sum(so_item.stock_qty)"
|
||||
select_field = "total_qty"
|
||||
|
||||
date_condition = get_date_condition(date_range, "so.transaction_date")
|
||||
if from_date and to_date:
|
||||
filters.append(["transaction_date", "between", [from_date, to_date]])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select so.customer as name, {0} as value
|
||||
FROM `tabSales Order` as so JOIN `tabSales Order Item` as so_item
|
||||
ON so.name = so_item.parent
|
||||
where so.docstatus = 1 {1} and so.company = %s
|
||||
group by so.customer
|
||||
order by value DESC
|
||||
limit %s
|
||||
""".format(
|
||||
select_field, date_condition
|
||||
),
|
||||
(company, cint(limit)),
|
||||
as_dict=1,
|
||||
return frappe.get_list(
|
||||
"Sales Order",
|
||||
fields=["customer as name", f"sum({select_field}) as value"],
|
||||
filters=filters,
|
||||
group_by="customer",
|
||||
order_by="value desc",
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@@ -96,55 +90,58 @@ def get_all_customers(date_range, company, field, limit=None):
|
||||
def get_all_items(date_range, company, field, limit=None):
|
||||
if field in ("available_stock_qty", "available_stock_value"):
|
||||
select_field = "sum(actual_qty)" if field == "available_stock_qty" else "sum(stock_value)"
|
||||
return frappe.db.get_all(
|
||||
results = frappe.db.get_all(
|
||||
"Bin",
|
||||
fields=["item_code as name", "{0} as value".format(select_field)],
|
||||
group_by="item_code",
|
||||
order_by="value desc",
|
||||
limit=limit,
|
||||
)
|
||||
readable_active_items = set(frappe.get_list("Item", filters={"disabled": 0}, pluck="name"))
|
||||
return [item for item in results if item["name"] in readable_active_items]
|
||||
else:
|
||||
if field == "total_sales_amount":
|
||||
select_field = "sum(order_item.base_net_amount)"
|
||||
select_field = "base_net_amount"
|
||||
select_doctype = "Sales Order"
|
||||
elif field == "total_purchase_amount":
|
||||
select_field = "sum(order_item.base_net_amount)"
|
||||
select_field = "base_net_amount"
|
||||
select_doctype = "Purchase Order"
|
||||
elif field == "total_qty_sold":
|
||||
select_field = "sum(order_item.stock_qty)"
|
||||
select_field = "stock_qty"
|
||||
select_doctype = "Sales Order"
|
||||
elif field == "total_qty_purchased":
|
||||
select_field = "sum(order_item.stock_qty)"
|
||||
select_field = "stock_qty"
|
||||
select_doctype = "Purchase Order"
|
||||
|
||||
date_condition = get_date_condition(date_range, "sales_order.transaction_date")
|
||||
filters = [["docstatus", "=", "1"], ["company", "=", company]]
|
||||
from_date, to_date = parse_date_range(date_range)
|
||||
if from_date and to_date:
|
||||
filters.append(["transaction_date", "between", [from_date, to_date]])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select order_item.item_code as name, {0} as value
|
||||
from `tab{1}` sales_order join `tab{1} Item` as order_item
|
||||
on sales_order.name = order_item.parent
|
||||
where sales_order.docstatus = 1
|
||||
and sales_order.company = %s {2}
|
||||
group by order_item.item_code
|
||||
order by value desc
|
||||
limit %s
|
||||
""".format(
|
||||
select_field, select_doctype, date_condition
|
||||
),
|
||||
(company, cint(limit)),
|
||||
as_dict=1,
|
||||
) # nosec
|
||||
child_doctype = f"{select_doctype} Item"
|
||||
return frappe.get_list(
|
||||
select_doctype,
|
||||
fields=[
|
||||
f"`tab{child_doctype}`.item_code as name",
|
||||
f"sum(`tab{child_doctype}`.{select_field}) as value",
|
||||
],
|
||||
filters=filters,
|
||||
order_by="value desc",
|
||||
group_by=f"`tab{child_doctype}`.item_code",
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_all_suppliers(date_range, company, field, limit=None):
|
||||
filters = [["docstatus", "=", "1"], ["company", "=", company]]
|
||||
from_date, to_date = parse_date_range(date_range)
|
||||
|
||||
if field == "outstanding_amount":
|
||||
filters = [["docstatus", "=", "1"], ["company", "=", company]]
|
||||
if date_range:
|
||||
date_range = frappe.parse_json(date_range)
|
||||
filters.append(["posting_date", "between", [date_range[0], date_range[1]]])
|
||||
return frappe.db.get_all(
|
||||
if from_date and to_date:
|
||||
filters.append(["posting_date", "between", [from_date, to_date]])
|
||||
|
||||
return frappe.get_list(
|
||||
"Purchase Invoice",
|
||||
fields=["supplier as name", "sum(outstanding_amount) as value"],
|
||||
filters=filters,
|
||||
@@ -154,48 +151,40 @@ def get_all_suppliers(date_range, company, field, limit=None):
|
||||
)
|
||||
else:
|
||||
if field == "total_purchase_amount":
|
||||
select_field = "sum(purchase_order_item.base_net_amount)"
|
||||
select_field = "base_net_total"
|
||||
elif field == "total_qty_purchased":
|
||||
select_field = "sum(purchase_order_item.stock_qty)"
|
||||
select_field = "total_qty"
|
||||
|
||||
date_condition = get_date_condition(date_range, "purchase_order.modified")
|
||||
if from_date and to_date:
|
||||
filters.append(["transaction_date", "between", [from_date, to_date]])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select purchase_order.supplier as name, {0} as value
|
||||
FROM `tabPurchase Order` as purchase_order LEFT JOIN `tabPurchase Order Item`
|
||||
as purchase_order_item ON purchase_order.name = purchase_order_item.parent
|
||||
where
|
||||
purchase_order.docstatus = 1
|
||||
{1}
|
||||
and purchase_order.company = %s
|
||||
group by purchase_order.supplier
|
||||
order by value DESC
|
||||
limit %s""".format(
|
||||
select_field, date_condition
|
||||
),
|
||||
(company, cint(limit)),
|
||||
as_dict=1,
|
||||
) # nosec
|
||||
return frappe.get_list(
|
||||
"Purchase Order",
|
||||
fields=["supplier as name", f"sum({select_field}) as value"],
|
||||
filters=filters,
|
||||
group_by="supplier",
|
||||
order_by="value desc",
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_all_sales_partner(date_range, company, field, limit=None):
|
||||
if field == "total_sales_amount":
|
||||
select_field = "sum(`base_net_total`)"
|
||||
select_field = "base_net_total"
|
||||
elif field == "total_commission":
|
||||
select_field = "sum(`total_commission`)"
|
||||
select_field = "total_commission"
|
||||
|
||||
filters = {"sales_partner": ["!=", ""], "docstatus": 1, "company": company}
|
||||
if date_range:
|
||||
date_range = frappe.parse_json(date_range)
|
||||
filters["transaction_date"] = ["between", [date_range[0], date_range[1]]]
|
||||
filters = [["docstatus", "=", "1"], ["company", "=", company], ["sales_partner", "is", "set"]]
|
||||
from_date, to_date = parse_date_range(date_range)
|
||||
if from_date and to_date:
|
||||
filters.append(["transaction_date", "between", [from_date, to_date]])
|
||||
|
||||
return frappe.get_list(
|
||||
"Sales Order",
|
||||
fields=[
|
||||
"`sales_partner` as name",
|
||||
"{} as value".format(select_field),
|
||||
"sales_partner as name",
|
||||
f"sum({select_field}) as value",
|
||||
],
|
||||
filters=filters,
|
||||
group_by="sales_partner",
|
||||
@@ -206,24 +195,25 @@ def get_all_sales_partner(date_range, company, field, limit=None):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_all_sales_person(date_range, company, field=None, limit=0):
|
||||
date_condition = get_date_condition(date_range, "sales_order.transaction_date")
|
||||
filters = [
|
||||
["docstatus", "=", "1"],
|
||||
["company", "=", company],
|
||||
["Sales Team", "sales_person", "is", "set"],
|
||||
]
|
||||
from_date, to_date = parse_date_range(date_range)
|
||||
if from_date and to_date:
|
||||
filters.append(["transaction_date", "between", [from_date, to_date]])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select sales_team.sales_person as name, sum(sales_order.base_net_total) as value
|
||||
from `tabSales Order` as sales_order join `tabSales Team` as sales_team
|
||||
on sales_order.name = sales_team.parent and sales_team.parenttype = 'Sales Order'
|
||||
where sales_order.docstatus = 1
|
||||
and sales_order.company = %s
|
||||
{date_condition}
|
||||
group by sales_team.sales_person
|
||||
order by value DESC
|
||||
limit %s
|
||||
""".format(
|
||||
date_condition=date_condition
|
||||
),
|
||||
(company, cint(limit)),
|
||||
as_dict=1,
|
||||
return frappe.get_list(
|
||||
"Sales Order",
|
||||
fields=[
|
||||
"`tabSales Team`.sales_person as name",
|
||||
"sum(`tabSales Team`.allocated_amount) as value",
|
||||
],
|
||||
filters=filters,
|
||||
group_by="`tabSales Team`.sales_person",
|
||||
order_by="value desc",
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@@ -236,3 +226,11 @@ def get_date_condition(date_range, field):
|
||||
field, frappe.db.escape(from_date), frappe.db.escape(to_date)
|
||||
)
|
||||
return date_condition
|
||||
|
||||
|
||||
def parse_date_range(date_range):
|
||||
if date_range:
|
||||
date_range = frappe.parse_json(date_range)
|
||||
return date_range[0], date_range[1]
|
||||
|
||||
return None, None
|
||||
|
||||
@@ -1184,6 +1184,21 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
frappe.db.rollback()
|
||||
frappe.db.set_single_value("Selling Settings", "dont_reserve_sales_order_qty_on_sales_return", 0)
|
||||
|
||||
def test_non_internal_transfer_delivery_note(self):
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
dn = create_delivery_note(do_not_submit=True)
|
||||
warehouse = create_warehouse("Internal Transfer Warehouse", company=dn.company)
|
||||
dn.items[0].db_set("target_warehouse", warehouse)
|
||||
|
||||
dn.reload()
|
||||
|
||||
self.assertEqual(dn.items[0].target_warehouse, warehouse)
|
||||
|
||||
dn.save()
|
||||
dn.reload()
|
||||
self.assertFalse(dn.items[0].target_warehouse)
|
||||
|
||||
|
||||
def create_delivery_note(**args):
|
||||
dn = frappe.new_doc("Delivery Note")
|
||||
|
||||
@@ -742,6 +742,8 @@ def create_item(
|
||||
opening_stock=0,
|
||||
is_fixed_asset=0,
|
||||
asset_category=None,
|
||||
buying_cost_center=None,
|
||||
selling_cost_center=None,
|
||||
company="_Test Company",
|
||||
):
|
||||
if not frappe.db.exists("Item", item_code):
|
||||
@@ -759,7 +761,15 @@ def create_item(
|
||||
item.is_purchase_item = is_purchase_item
|
||||
item.is_customer_provided_item = is_customer_provided_item
|
||||
item.customer = customer or ""
|
||||
item.append("item_defaults", {"default_warehouse": warehouse, "company": company})
|
||||
item.append(
|
||||
"item_defaults",
|
||||
{
|
||||
"default_warehouse": warehouse,
|
||||
"company": company,
|
||||
"selling_cost_center": selling_cost_center,
|
||||
"buying_cost_center": buying_cost_center,
|
||||
},
|
||||
)
|
||||
item.save()
|
||||
else:
|
||||
item = frappe.get_doc("Item", item_code)
|
||||
|
||||
@@ -1069,88 +1069,6 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
pr1.reload()
|
||||
pr1.cancel()
|
||||
|
||||
def test_stock_transfer_from_purchase_receipt(self):
|
||||
pr1 = make_purchase_receipt(
|
||||
warehouse="Work In Progress - TCP1", company="_Test Company with perpetual inventory"
|
||||
)
|
||||
|
||||
pr = make_purchase_receipt(
|
||||
company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", do_not_save=1
|
||||
)
|
||||
|
||||
pr.supplier_warehouse = ""
|
||||
pr.items[0].from_warehouse = "Work In Progress - TCP1"
|
||||
|
||||
pr.submit()
|
||||
|
||||
gl_entries = get_gl_entries("Purchase Receipt", pr.name)
|
||||
sl_entries = get_sl_entries("Purchase Receipt", pr.name)
|
||||
|
||||
self.assertFalse(gl_entries)
|
||||
|
||||
expected_sle = {"Work In Progress - TCP1": -5, "Stores - TCP1": 5}
|
||||
|
||||
for sle in sl_entries:
|
||||
self.assertEqual(expected_sle[sle.warehouse], sle.actual_qty)
|
||||
|
||||
pr.cancel()
|
||||
pr1.cancel()
|
||||
|
||||
def test_stock_transfer_from_purchase_receipt_with_valuation(self):
|
||||
create_warehouse(
|
||||
"_Test Warehouse for Valuation",
|
||||
company="_Test Company with perpetual inventory",
|
||||
properties={"account": "_Test Account Stock In Hand - TCP1"},
|
||||
)
|
||||
|
||||
pr1 = make_purchase_receipt(
|
||||
warehouse="_Test Warehouse for Valuation - TCP1",
|
||||
company="_Test Company with perpetual inventory",
|
||||
)
|
||||
|
||||
pr = make_purchase_receipt(
|
||||
company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", do_not_save=1
|
||||
)
|
||||
|
||||
pr.items[0].from_warehouse = "_Test Warehouse for Valuation - TCP1"
|
||||
pr.supplier_warehouse = ""
|
||||
|
||||
pr.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "_Test Account Shipping Charges - TCP1",
|
||||
"category": "Valuation and Total",
|
||||
"cost_center": "Main - TCP1",
|
||||
"description": "Test",
|
||||
"rate": 9,
|
||||
},
|
||||
)
|
||||
|
||||
pr.submit()
|
||||
|
||||
gl_entries = get_gl_entries("Purchase Receipt", pr.name)
|
||||
sl_entries = get_sl_entries("Purchase Receipt", pr.name)
|
||||
|
||||
expected_gle = [
|
||||
["Stock In Hand - TCP1", 272.5, 0.0],
|
||||
["_Test Account Stock In Hand - TCP1", 0.0, 250.0],
|
||||
["_Test Account Shipping Charges - TCP1", 0.0, 22.5],
|
||||
]
|
||||
|
||||
expected_sle = {"_Test Warehouse for Valuation - TCP1": -5, "Stores - TCP1": 5}
|
||||
|
||||
for sle in sl_entries:
|
||||
self.assertEqual(expected_sle[sle.warehouse], sle.actual_qty)
|
||||
|
||||
for i, gle in enumerate(gl_entries):
|
||||
self.assertEqual(gle.account, expected_gle[i][0])
|
||||
self.assertEqual(gle.debit, expected_gle[i][1])
|
||||
self.assertEqual(gle.credit, expected_gle[i][2])
|
||||
|
||||
pr.cancel()
|
||||
pr1.cancel()
|
||||
|
||||
def test_subcontracted_pr_for_multi_transfer_batches(self):
|
||||
from erpnext.buying.doctype.purchase_order.purchase_order import (
|
||||
make_purchase_receipt,
|
||||
@@ -1996,6 +1914,75 @@ class TestPurchaseReceipt(FrappeTestCase):
|
||||
ste7.reload()
|
||||
self.assertEqual(ste7.items[0].valuation_rate, valuation_rate)
|
||||
|
||||
def test_valuation_rate_in_return_purchase_receipt_for_moving_average(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
from erpnext.stock.stock_ledger import get_previous_sle
|
||||
|
||||
# Step - 1: Create an Item (Valuation Method = Moving Average)
|
||||
item_code = make_item(properties={"is_stock_item": 1, "valuation_method": "Moving Average"}).name
|
||||
|
||||
# Step - 2: Create a Purchase Receipt (Qty = 10, Rate = 100)
|
||||
pr = make_purchase_receipt(qty=10, rate=100, item_code=item_code)
|
||||
|
||||
# Step - 3: Create a Material Receipt Stock Entry (Qty = 100, Basic Rate = 10)
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
make_stock_entry(
|
||||
purpose="Material Receipt",
|
||||
item_code=item_code,
|
||||
to_warehouse=warehouse,
|
||||
qty=100,
|
||||
rate=10,
|
||||
)
|
||||
|
||||
# Step - 4: Create a Material Issue Stock Entry (Qty = 100, Basic Rate = 18.18 [Auto Fetched])
|
||||
make_stock_entry(
|
||||
purpose="Material Issue", item_code=item_code, from_warehouse=warehouse, qty=100
|
||||
)
|
||||
|
||||
# Step - 5: Create a Return Purchase Return (Qty = -8, Rate = 100 [Auto fetched])
|
||||
return_pr = make_purchase_receipt(
|
||||
is_return=1,
|
||||
return_against=pr.name,
|
||||
item_code=item_code,
|
||||
qty=-8,
|
||||
)
|
||||
|
||||
sle = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_no": return_pr.name, "voucher_detail_no": return_pr.items[0].name},
|
||||
["posting_date", "posting_time", "outgoing_rate", "valuation_rate"],
|
||||
as_dict=1,
|
||||
)
|
||||
previous_sle_valuation_rate = get_previous_sle(
|
||||
{
|
||||
"item_code": item_code,
|
||||
"warehouse": warehouse,
|
||||
"posting_date": sle.posting_date,
|
||||
"posting_time": sle.posting_time,
|
||||
}
|
||||
).get("valuation_rate")
|
||||
|
||||
# Test - 1: Valuation Rate should be equal to Outgoing Rate
|
||||
self.assertEqual(flt(sle.outgoing_rate, 2), flt(sle.valuation_rate, 2))
|
||||
|
||||
# Test - 2: Valuation Rate should be equal to Previous SLE Valuation Rate
|
||||
self.assertEqual(flt(sle.valuation_rate, 2), flt(previous_sle_valuation_rate, 2))
|
||||
|
||||
def non_internal_transfer_purchase_receipt(self):
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
pr_doc = make_purchase_receipt(do_not_submit=True)
|
||||
warehouse = create_warehouse("Internal Transfer Warehouse", pr_doc.company)
|
||||
pr_doc.items[0].db_set("target_warehouse", "warehouse")
|
||||
|
||||
pr_doc.reload()
|
||||
|
||||
self.assertEqual(pr_doc.items[0].from_warehouse, warehouse.name)
|
||||
|
||||
pr_doc.save()
|
||||
pr_doc.reload()
|
||||
self.assertFalse(pr_doc.items[0].from_warehouse)
|
||||
|
||||
|
||||
def prepare_data_for_internal_transfer():
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
from unittest.mock import MagicMock, call
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import nowdate
|
||||
from frappe.utils.data import add_to_date, today
|
||||
|
||||
@@ -173,6 +173,7 @@ class TestRepostItemValuation(FrappeTestCase, StockTestMixin):
|
||||
|
||||
riv.set_status("Skipped")
|
||||
|
||||
@change_settings("Stock Reposting Settings", {"item_based_reposting": 0})
|
||||
def test_prevention_of_cancelled_transaction_riv(self):
|
||||
frappe.flags.dont_execute_stock_reposts = True
|
||||
|
||||
@@ -295,6 +296,7 @@ class TestRepostItemValuation(FrappeTestCase, StockTestMixin):
|
||||
accounts_settings.acc_frozen_upto = ""
|
||||
accounts_settings.save()
|
||||
|
||||
@change_settings("Stock Reposting Settings", {"item_based_reposting": 0})
|
||||
def test_create_repost_entry_for_cancelled_document(self):
|
||||
pr = make_purchase_receipt(
|
||||
company="_Test Company with perpetual inventory",
|
||||
|
||||
@@ -1414,6 +1414,7 @@ class TestStockEntry(FrappeTestCase):
|
||||
self.assertEqual(se.items[0].item_name, item.item_name)
|
||||
self.assertEqual(se.items[0].stock_uom, item.stock_uom)
|
||||
|
||||
@change_settings("Stock Reposting Settings", {"item_based_reposting": 0})
|
||||
def test_reposting_for_depedent_warehouse(self):
|
||||
from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import repost_sl_entries
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"label": "Limit timeslot for Stock Reposting"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"default": "1",
|
||||
"fieldname": "item_based_reposting",
|
||||
"fieldtype": "Check",
|
||||
"label": "Use Item based reposting"
|
||||
@@ -57,7 +57,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-11-02 01:22:45.155841",
|
||||
"modified": "2023-11-01 16:14:29.080697",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Reposting Settings",
|
||||
@@ -77,4 +77,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -751,6 +751,12 @@ def get_default_cost_center(args, item=None, item_group=None, brand=None, compan
|
||||
data = frappe.get_attr(path)(args.get("item_code"), company)
|
||||
|
||||
if data and (data.selling_cost_center or data.buying_cost_center):
|
||||
if args.get("customer") and data.selling_cost_center:
|
||||
return data.selling_cost_center
|
||||
|
||||
elif args.get("supplier") and data.buying_cost_center:
|
||||
return data.buying_cost_center
|
||||
|
||||
return data.selling_cost_center or data.buying_cost_center
|
||||
|
||||
if not cost_center and args.get("cost_center"):
|
||||
|
||||
@@ -13,10 +13,18 @@ const DIFFERENCE_FIELD_NAMES = [
|
||||
|
||||
frappe.query_reports["Stock Ledger Variance"] = {
|
||||
"filters": [
|
||||
{
|
||||
"fieldname": "company",
|
||||
"label": __("Company"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Company",
|
||||
"reqd": 1,
|
||||
"default": frappe.defaults.get_user_default("Company")
|
||||
},
|
||||
{
|
||||
"fieldname": "item_code",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"label": __("Item"),
|
||||
"options": "Item",
|
||||
get_query: function() {
|
||||
return {
|
||||
@@ -27,7 +35,7 @@ frappe.query_reports["Stock Ledger Variance"] = {
|
||||
{
|
||||
"fieldname": "warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Warehouse",
|
||||
"label": __("Warehouse"),
|
||||
"options": "Warehouse",
|
||||
get_query: function() {
|
||||
return {
|
||||
@@ -38,7 +46,7 @@ frappe.query_reports["Stock Ledger Variance"] = {
|
||||
{
|
||||
"fieldname": "difference_in",
|
||||
"fieldtype": "Select",
|
||||
"label": "Difference In",
|
||||
"label": __("Difference In"),
|
||||
"options": [
|
||||
"",
|
||||
"Qty",
|
||||
@@ -49,7 +57,7 @@ frappe.query_reports["Stock Ledger Variance"] = {
|
||||
{
|
||||
"fieldname": "include_disabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Include Disabled",
|
||||
"label": __("Include Disabled"),
|
||||
}
|
||||
],
|
||||
|
||||
|
||||
@@ -55,6 +55,11 @@ def get_columns():
|
||||
"label": _("Warehouse"),
|
||||
"options": "Warehouse",
|
||||
},
|
||||
{
|
||||
"fieldname": "valuation_method",
|
||||
"fieldtype": "Data",
|
||||
"label": _("Valuation Method"),
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_type",
|
||||
"fieldtype": "Link",
|
||||
@@ -194,6 +199,7 @@ def get_columns():
|
||||
def get_data(filters=None):
|
||||
filters = frappe._dict(filters or {})
|
||||
item_warehouse_map = get_item_warehouse_combinations(filters)
|
||||
valuation_method = frappe.db.get_single_value("Stock Settings", "valuation_method")
|
||||
|
||||
data = []
|
||||
if item_warehouse_map:
|
||||
@@ -206,8 +212,17 @@ def get_data(filters=None):
|
||||
continue
|
||||
|
||||
for row in report_data:
|
||||
if has_difference(row, precision, filters.difference_in):
|
||||
data.append(add_item_warehouse_details(row, item_warehouse))
|
||||
if has_difference(
|
||||
row, precision, filters.difference_in, item_warehouse.valuation_method or valuation_method
|
||||
):
|
||||
row.update(
|
||||
{
|
||||
"item_code": item_warehouse.item_code,
|
||||
"warehouse": item_warehouse.warehouse,
|
||||
"valuation_method": item_warehouse.valuation_method or valuation_method,
|
||||
}
|
||||
)
|
||||
data.append(row)
|
||||
break
|
||||
|
||||
return data
|
||||
@@ -229,8 +244,14 @@ def get_item_warehouse_combinations(filters: dict = None) -> dict:
|
||||
.select(
|
||||
bin.item_code,
|
||||
bin.warehouse,
|
||||
item.valuation_method,
|
||||
)
|
||||
.where(
|
||||
(item.is_stock_item == 1)
|
||||
& (item.has_serial_no == 0)
|
||||
& (warehouse.is_group == 0)
|
||||
& (warehouse.company == filters.company)
|
||||
)
|
||||
.where((item.is_stock_item == 1) & (item.has_serial_no == 0) & (warehouse.is_group == 0))
|
||||
)
|
||||
|
||||
if filters.item_code:
|
||||
@@ -243,37 +264,27 @@ def get_item_warehouse_combinations(filters: dict = None) -> dict:
|
||||
return query.run(as_dict=1)
|
||||
|
||||
|
||||
def has_difference(row, precision, difference_in):
|
||||
has_qty_difference = flt(row.difference_in_qty, precision) or flt(row.fifo_qty_diff, precision)
|
||||
has_value_difference = (
|
||||
flt(row.diff_value_diff, precision)
|
||||
or flt(row.fifo_value_diff, precision)
|
||||
or flt(row.fifo_difference_diff, precision)
|
||||
)
|
||||
has_valuation_difference = flt(row.valuation_diff, precision) or flt(
|
||||
row.fifo_valuation_diff, precision
|
||||
)
|
||||
def has_difference(row, precision, difference_in, valuation_method):
|
||||
if valuation_method == "Moving Average":
|
||||
qty_diff = flt(row.difference_in_qty, precision)
|
||||
value_diff = flt(row.diff_value_diff, precision)
|
||||
valuation_diff = flt(row.valuation_diff, precision)
|
||||
else:
|
||||
qty_diff = flt(row.difference_in_qty, precision) or flt(row.fifo_qty_diff, precision)
|
||||
value_diff = (
|
||||
flt(row.diff_value_diff, precision)
|
||||
or flt(row.fifo_value_diff, precision)
|
||||
or flt(row.fifo_difference_diff, precision)
|
||||
)
|
||||
valuation_diff = flt(row.valuation_diff, precision) or flt(row.fifo_valuation_diff, precision)
|
||||
|
||||
if difference_in == "Qty" and has_qty_difference:
|
||||
if difference_in == "Qty" and qty_diff:
|
||||
return True
|
||||
elif difference_in == "Value" and has_value_difference:
|
||||
elif difference_in == "Value" and value_diff:
|
||||
return True
|
||||
elif difference_in == "Valuation" and has_valuation_difference:
|
||||
elif difference_in == "Valuation" and valuation_diff:
|
||||
return True
|
||||
elif difference_in not in ["Qty", "Value", "Valuation"] and (
|
||||
has_qty_difference or has_value_difference or has_valuation_difference
|
||||
qty_diff or value_diff or valuation_diff
|
||||
):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def add_item_warehouse_details(row, item_warehouse):
|
||||
row.update(
|
||||
{
|
||||
"item_code": item_warehouse.item_code,
|
||||
"warehouse": item_warehouse.warehouse,
|
||||
}
|
||||
)
|
||||
|
||||
return row
|
||||
|
||||
@@ -658,14 +658,16 @@ class update_entries_after(object):
|
||||
get_rate_for_return, # don't move this import to top
|
||||
)
|
||||
|
||||
rate = get_rate_for_return(
|
||||
sle.voucher_type,
|
||||
sle.voucher_no,
|
||||
sle.item_code,
|
||||
voucher_detail_no=sle.voucher_detail_no,
|
||||
sle=sle,
|
||||
)
|
||||
|
||||
if self.valuation_method == "Moving Average":
|
||||
rate = flt(self.data[self.args.warehouse].previous_sle.valuation_rate)
|
||||
else:
|
||||
rate = get_rate_for_return(
|
||||
sle.voucher_type,
|
||||
sle.voucher_no,
|
||||
sle.item_code,
|
||||
voucher_detail_no=sle.voucher_detail_no,
|
||||
sle=sle,
|
||||
)
|
||||
elif (
|
||||
sle.voucher_type in ["Purchase Receipt", "Purchase Invoice"]
|
||||
and sle.voucher_detail_no
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
{% if d.thumbnail or d.image %}
|
||||
{{ product_image(d.thumbnail or d.image, no_border=True) }}
|
||||
{% else %}
|
||||
<div class="no-image-cart-item" style="min-height: 100px;">
|
||||
<div class="no-image-cart-item" style="min-height: 50px;">
|
||||
{{ frappe.utils.get_abbr(d.item_name) or "NA" }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -81,7 +81,7 @@ rfq = Class.extend({
|
||||
doc: doc
|
||||
},
|
||||
btn: this,
|
||||
callback: function(r){
|
||||
callback: function(r) {
|
||||
frappe.unfreeze();
|
||||
if(r.message){
|
||||
$('.btn-sm').hide()
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
{% from "erpnext/templates/includes/macros.html" import product_image_square, product_image %}
|
||||
|
||||
{% macro item_name_and_description(d, doc) %}
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
{{ product_image(d.image) }}
|
||||
</div>
|
||||
<div class="col-9">
|
||||
{{ d.item_code }}
|
||||
<p class="text-muted small">{{ d.description }}</p>
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
{% if d.image %}
|
||||
{{ product_image(d.image) }}
|
||||
{% else %}
|
||||
<div class="website-image h-100 w-100" style="background-color:var(--gray-100);text-align: center;line-height: 3.6;">
|
||||
{{ frappe.utils.get_abbr(d.item_name)}}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-9">
|
||||
{{ d.item_code }}
|
||||
<p class="text-muted small">{{ d.description }}</p>
|
||||
{% set supplier_part_no = frappe.db.get_value("Item Supplier", {'parent': d.item_code, 'supplier': doc.supplier}, "supplier_part_no") %}
|
||||
<p class="text-muted small supplier-part-no">
|
||||
{% if supplier_part_no %}
|
||||
{{_("Supplier Part No") + ": "+ supplier_part_no}}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
@@ -153,7 +153,6 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if attachments %}
|
||||
<div class="order-item-table">
|
||||
<div class="row order-items order-item-header text-muted">
|
||||
@@ -181,6 +180,7 @@
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block script %}
|
||||
<script> {% include "templates/pages/order.js" %} </script>
|
||||
<script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "templates/web.html" %}
|
||||
|
||||
{% block header %}
|
||||
<h1>{{ doc.name }}</h1>
|
||||
<h1 style="margin-top: 10px;">{{ doc.name }}</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
@@ -16,7 +16,7 @@
|
||||
{% if doc.items %}
|
||||
<button class="btn btn-primary btn-sm"
|
||||
type="button">
|
||||
{{ _("Submit") }}</button>
|
||||
{{ _("Make Quotation") }}</button>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@@ -369,7 +369,7 @@ Base,Basis,
|
||||
Base URL,Basis-URL,
|
||||
Based On,Basiert auf,
|
||||
Based On Payment Terms,Basierend auf Zahlungsbedingungen,
|
||||
Basic,Grundeinkommen,
|
||||
Basic,Basic,
|
||||
Batch,Charge,
|
||||
Batch Entries,Batch-Einträge,
|
||||
Batch ID is mandatory,Batch-ID ist obligatorisch,
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -4,7 +4,7 @@ googlemaps # used in ERPNext, but dependency is defined in Frappe
|
||||
pandas>=1.1.5,<2.0.0
|
||||
plaid-python~=7.2.1
|
||||
pycountry~=20.7.3
|
||||
PyGithub~=1.54.1
|
||||
PyGithub~=2.1.1
|
||||
python-stdnum~=1.16
|
||||
python-youtube~=0.8.0
|
||||
taxjar~=1.9.2
|
||||
|
||||
Reference in New Issue
Block a user