mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-18 04:29:18 +00:00
Compare commits
131 Commits
v13.52.10
...
mergify/bp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3b8735222 | ||
|
|
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 | ||
|
|
b0708d29a8 | ||
|
|
caa8417306 | ||
|
|
ff6b38c9e7 | ||
|
|
4285bbcdc0 | ||
|
|
b6dc47ec8a | ||
|
|
d0c6f286cf | ||
|
|
a6bef64c8e | ||
|
|
5092ea175e | ||
|
|
5833c4dae2 | ||
|
|
48eb6a6573 | ||
|
|
7c1288f726 | ||
|
|
c89715d6da | ||
|
|
d4f8b057e1 | ||
|
|
86b152fe5c | ||
|
|
11b16569e5 | ||
|
|
cd99630457 | ||
|
|
efab1e7361 | ||
|
|
63a6e7e35c | ||
|
|
d7f09f8795 | ||
|
|
437a294621 | ||
|
|
9a15ed8083 | ||
|
|
3a49d4f9b3 | ||
|
|
8b2c6ed61a | ||
|
|
4f98e958a1 | ||
|
|
3a71fa9d96 | ||
|
|
6f40d0cdf6 | ||
|
|
5bd6c27b05 | ||
|
|
9876019c69 | ||
|
|
f4fb878282 | ||
|
|
783bb93913 | ||
|
|
cfbd9af100 | ||
|
|
966c296872 | ||
|
|
0ff871e38e | ||
|
|
9df10dbc40 | ||
|
|
bdaae81171 | ||
|
|
066cf0e3bc | ||
|
|
829298066f | ||
|
|
006da22d3f | ||
|
|
3b9ac9f46a |
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.52.10"
|
||||
__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()
|
||||
|
||||
@@ -261,9 +261,7 @@ class PurchaseInvoice(BuyingController):
|
||||
stock_not_billed_account = self.get_company_default("stock_received_but_not_billed")
|
||||
stock_items = self.get_stock_items()
|
||||
|
||||
asset_items = [d.is_fixed_asset for d in self.items if d.is_fixed_asset]
|
||||
if len(asset_items) > 0:
|
||||
asset_received_but_not_billed = self.get_company_default("asset_received_but_not_billed")
|
||||
asset_received_but_not_billed = None
|
||||
|
||||
if self.update_stock:
|
||||
self.validate_item_code()
|
||||
@@ -357,6 +355,8 @@ class PurchaseInvoice(BuyingController):
|
||||
)
|
||||
item.expense_account = asset_category_account
|
||||
elif item.is_fixed_asset and item.pr_detail:
|
||||
if not asset_received_but_not_billed:
|
||||
asset_received_but_not_billed = self.get_company_default("asset_received_but_not_billed")
|
||||
item.expense_account = asset_received_but_not_billed
|
||||
elif not item.expense_account and for_validate:
|
||||
throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name))
|
||||
@@ -924,8 +924,9 @@ class PurchaseInvoice(BuyingController):
|
||||
)
|
||||
|
||||
def get_asset_gl_entry(self, gl_entries):
|
||||
arbnb_account = self.get_company_default("asset_received_but_not_billed")
|
||||
eiiav_account = self.get_company_default("expenses_included_in_asset_valuation")
|
||||
arbnb_account = None
|
||||
eiiav_account = None
|
||||
asset_eiiav_currency = None
|
||||
|
||||
for item in self.get("items"):
|
||||
if item.is_fixed_asset:
|
||||
@@ -937,6 +938,8 @@ class PurchaseInvoice(BuyingController):
|
||||
"Asset Received But Not Billed",
|
||||
"Fixed Asset",
|
||||
]:
|
||||
if not arbnb_account:
|
||||
arbnb_account = self.get_company_default("asset_received_but_not_billed")
|
||||
item.expense_account = arbnb_account
|
||||
|
||||
if not self.update_stock:
|
||||
@@ -959,7 +962,10 @@ class PurchaseInvoice(BuyingController):
|
||||
)
|
||||
|
||||
if item.item_tax_amount:
|
||||
asset_eiiav_currency = get_account_currency(eiiav_account)
|
||||
if not eiiav_account or not asset_eiiav_currency:
|
||||
eiiav_account = self.get_company_default("expenses_included_in_asset_valuation")
|
||||
asset_eiiav_currency = get_account_currency(eiiav_account)
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
@@ -1002,7 +1008,10 @@ class PurchaseInvoice(BuyingController):
|
||||
)
|
||||
|
||||
if item.item_tax_amount and not cint(erpnext.is_perpetual_inventory_enabled(self.company)):
|
||||
asset_eiiav_currency = get_account_currency(eiiav_account)
|
||||
if not eiiav_account or not asset_eiiav_currency:
|
||||
eiiav_account = self.get_company_default("expenses_included_in_asset_valuation")
|
||||
asset_eiiav_currency = get_account_currency(eiiav_account)
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
@@ -1022,47 +1031,46 @@ class PurchaseInvoice(BuyingController):
|
||||
)
|
||||
)
|
||||
|
||||
# When update stock is checked
|
||||
# Assets are bought through this document then it will be linked to this document
|
||||
if self.update_stock:
|
||||
if flt(item.landed_cost_voucher_amount):
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": eiiav_account,
|
||||
"against": cwip_account,
|
||||
"cost_center": item.cost_center,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"credit": flt(item.landed_cost_voucher_amount),
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
if flt(item.landed_cost_voucher_amount):
|
||||
if not eiiav_account:
|
||||
eiiav_account = self.get_company_default("expenses_included_in_asset_valuation")
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": cwip_account,
|
||||
"against": eiiav_account,
|
||||
"cost_center": item.cost_center,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"debit": flt(item.landed_cost_voucher_amount),
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
item=item,
|
||||
)
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": eiiav_account,
|
||||
"against": cwip_account,
|
||||
"cost_center": item.cost_center,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"credit": flt(item.landed_cost_voucher_amount),
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
item=item,
|
||||
)
|
||||
|
||||
# update gross amount of assets bought through this document
|
||||
assets = frappe.db.get_all(
|
||||
"Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code}
|
||||
)
|
||||
for asset in assets:
|
||||
frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate))
|
||||
frappe.db.set_value(
|
||||
"Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate)
|
||||
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": cwip_account,
|
||||
"against": eiiav_account,
|
||||
"cost_center": item.cost_center,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"debit": flt(item.landed_cost_voucher_amount),
|
||||
"project": item.project or self.project,
|
||||
},
|
||||
item=item,
|
||||
)
|
||||
)
|
||||
|
||||
# update gross amount of assets bought through this document
|
||||
assets = frappe.db.get_all(
|
||||
"Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code}
|
||||
)
|
||||
for asset in assets:
|
||||
frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate))
|
||||
frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate))
|
||||
|
||||
return gl_entries
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1783,6 +1783,10 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
)
|
||||
|
||||
def test_outstanding_amount_after_advance_payment_entry_cancellation(self):
|
||||
"""Test impact of advance PE submission/cancellation on SI and SO."""
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
|
||||
sales_order = make_sales_order(item_code="138-CMS Shoe", qty=1, price_list_rate=500)
|
||||
pe = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Payment Entry",
|
||||
@@ -1802,10 +1806,25 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
"paid_to": "_Test Cash - _TC",
|
||||
}
|
||||
)
|
||||
pe.append(
|
||||
"references",
|
||||
{
|
||||
"reference_doctype": "Sales Order",
|
||||
"reference_name": sales_order.name,
|
||||
"total_amount": sales_order.grand_total,
|
||||
"outstanding_amount": sales_order.grand_total,
|
||||
"allocated_amount": 300,
|
||||
},
|
||||
)
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
sales_order.reload()
|
||||
self.assertEqual(sales_order.advance_paid, 300)
|
||||
|
||||
si = frappe.copy_doc(test_records[0])
|
||||
si.items[0].sales_order = sales_order.name
|
||||
si.items[0].so_detail = sales_order.get("items")[0].name
|
||||
si.is_pos = 0
|
||||
si.append(
|
||||
"advances",
|
||||
@@ -1813,6 +1832,7 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
"doctype": "Sales Invoice Advance",
|
||||
"reference_type": "Payment Entry",
|
||||
"reference_name": pe.name,
|
||||
"reference_row": pe.references[0].name,
|
||||
"advance_amount": 300,
|
||||
"allocated_amount": 300,
|
||||
"remarks": pe.remarks,
|
||||
@@ -1821,7 +1841,13 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
si.load_from_db()
|
||||
si.reload()
|
||||
pe.reload()
|
||||
sales_order.reload()
|
||||
|
||||
# Check if SO is unlinked/replaced by SI in PE & if SO advance paid is 0
|
||||
self.assertEqual(pe.references[0].reference_name, si.name)
|
||||
self.assertEqual(sales_order.advance_paid, 0.0)
|
||||
|
||||
# check outstanding after advance allocation
|
||||
self.assertEqual(
|
||||
@@ -1829,11 +1855,9 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
flt(si.rounded_total - si.total_advance, si.precision("outstanding_amount")),
|
||||
)
|
||||
|
||||
# added to avoid Document has been modified exception
|
||||
pe = frappe.get_doc("Payment Entry", pe.name)
|
||||
pe.cancel()
|
||||
si.reload()
|
||||
|
||||
si.load_from_db()
|
||||
# check outstanding after advance cancellation
|
||||
self.assertEqual(
|
||||
flt(si.outstanding_amount),
|
||||
@@ -2448,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",
|
||||
|
||||
@@ -694,3 +694,23 @@ class TestSubscription(unittest.TestCase):
|
||||
# Check the currency of the created invoice
|
||||
currency = frappe.db.get_value("Sales Invoice", subscription.invoices[0].invoice, "currency")
|
||||
self.assertEqual(currency, "USD")
|
||||
|
||||
def test_plan_rate_for_midmonth_start_date(self):
|
||||
subscription = frappe.new_doc("Subscription")
|
||||
subscription.party_type = "Supplier"
|
||||
subscription.party = "_Test Supplier"
|
||||
subscription.generate_invoice_at_period_start = 1
|
||||
subscription.follow_calendar_months = 1
|
||||
subscription.generate_new_invoices_past_due_date = 1
|
||||
subscription.start_date = "2023-04-08"
|
||||
subscription.end_date = "2024-02-27"
|
||||
subscription.append("plans", {"plan": "_Test Plan Name 4", "qty": 1})
|
||||
subscription.save()
|
||||
|
||||
subscription.process()
|
||||
|
||||
self.assertEqual(len(subscription.invoices), 1)
|
||||
pi = frappe.get_doc("Purchase Invoice", subscription.invoices[0].invoice)
|
||||
self.assertEqual(pi.total, 55333.33)
|
||||
|
||||
subscription.delete()
|
||||
|
||||
@@ -56,18 +56,17 @@ def get_plan_rate(
|
||||
prorate = frappe.db.get_single_value("Subscription Settings", "prorate")
|
||||
|
||||
if prorate:
|
||||
prorate_factor = flt(
|
||||
date_diff(start_date, get_first_day(start_date))
|
||||
/ date_diff(get_last_day(start_date), get_first_day(start_date)),
|
||||
1,
|
||||
)
|
||||
|
||||
prorate_factor += flt(
|
||||
date_diff(get_last_day(end_date), end_date)
|
||||
/ date_diff(get_last_day(end_date), get_first_day(end_date)),
|
||||
1,
|
||||
)
|
||||
|
||||
cost -= plan.cost * prorate_factor
|
||||
|
||||
cost -= plan.cost * get_prorate_factor(start_date, end_date)
|
||||
return cost
|
||||
|
||||
|
||||
def get_prorate_factor(start_date, end_date):
|
||||
total_days_to_skip = date_diff(start_date, get_first_day(start_date))
|
||||
total_days_in_month = int(get_last_day(start_date).strftime("%d"))
|
||||
prorate_factor = flt(total_days_to_skip / total_days_in_month)
|
||||
|
||||
total_days_to_skip = date_diff(get_last_day(end_date), end_date)
|
||||
total_days_in_month = int(get_last_day(end_date).strftime("%d"))
|
||||
prorate_factor += flt(total_days_to_skip / total_days_in_month)
|
||||
|
||||
return prorate_factor
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -400,12 +400,20 @@ def calculate_values(accounts_by_name, gl_entries_by_account, companies, filters
|
||||
for entries in gl_entries_by_account.values():
|
||||
for entry in entries:
|
||||
if entry.account_number:
|
||||
<<<<<<< HEAD
|
||||
account_name = entry.account_number + " - " + entry.account_name
|
||||
else:
|
||||
account_name = entry.account_name
|
||||
|
||||
d = accounts_by_name.get(account_name)
|
||||
|
||||
=======
|
||||
account_name = entry.account_number + ' - ' + entry.account_name
|
||||
else:
|
||||
account_name = entry.account_name
|
||||
|
||||
d = accounts_by_name.get(account_name)
|
||||
>>>>>>> 625626b973 (fix: Values with same account and different account number in consolidated balance sheet report (#27493))
|
||||
if d:
|
||||
debit, credit = 0, 0
|
||||
for company in companies:
|
||||
@@ -482,9 +490,15 @@ def update_parent_account_names(accounts):
|
||||
|
||||
for d in accounts:
|
||||
if d.account_number:
|
||||
<<<<<<< HEAD
|
||||
account_name = d.account_number + " - " + d.account_name
|
||||
else:
|
||||
account_name = d.account_name
|
||||
=======
|
||||
account_name = d.account_number + ' - ' + d.account_name
|
||||
else:
|
||||
account_name = d.account_name
|
||||
>>>>>>> 625626b973 (fix: Values with same account and different account number in consolidated balance sheet report (#27493))
|
||||
name_to_account_map[d.name] = account_name
|
||||
|
||||
for account in accounts:
|
||||
@@ -660,9 +674,15 @@ def set_gl_entries_by_account(
|
||||
|
||||
for entry in gl_entries:
|
||||
if entry.account_number:
|
||||
<<<<<<< HEAD
|
||||
account_name = entry.account_number + " - " + entry.account_name
|
||||
else:
|
||||
account_name = entry.account_name
|
||||
=======
|
||||
account_name = entry.account_number + ' - ' + entry.account_name
|
||||
else:
|
||||
account_name = entry.account_name
|
||||
>>>>>>> 625626b973 (fix: Values with same account and different account number in consolidated balance sheet report (#27493))
|
||||
|
||||
validate_entries(account_name, entry, accounts_by_name, accounts)
|
||||
gl_entries_by_account.setdefault(account_name, []).append(entry)
|
||||
@@ -770,10 +790,16 @@ def filter_accounts(accounts, depth=10):
|
||||
accounts_by_name = {}
|
||||
for d in accounts:
|
||||
if d.account_number:
|
||||
<<<<<<< HEAD
|
||||
account_name = d.account_number + " - " + d.account_name
|
||||
else:
|
||||
account_name = d.account_name
|
||||
d["company_wise_opening_bal"] = defaultdict(float)
|
||||
=======
|
||||
account_name = d.account_number + ' - ' + d.account_name
|
||||
else:
|
||||
account_name = d.account_name
|
||||
>>>>>>> 625626b973 (fix: Values with same account and different account number in consolidated balance sheet report (#27493))
|
||||
accounts_by_name[account_name] = d
|
||||
|
||||
parent_children_map.setdefault(d.parent_account or None, []).append(d)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -537,6 +537,10 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
|
||||
"""
|
||||
jv_detail = journal_entry.get("accounts", {"name": d["voucher_detail_no"]})[0]
|
||||
|
||||
# Update Advance Paid in SO/PO since they might be getting unlinked
|
||||
if jv_detail.get("reference_type") in ("Sales Order", "Purchase Order"):
|
||||
frappe.get_doc(jv_detail.reference_type, jv_detail.reference_name).set_total_advance_paid()
|
||||
|
||||
if flt(d["unadjusted_amount"]) - flt(d["allocated_amount"]) != 0:
|
||||
# adjust the unreconciled balance
|
||||
amount_in_account_currency = flt(d["unadjusted_amount"]) - flt(d["allocated_amount"])
|
||||
@@ -596,6 +600,13 @@ def update_reference_in_payment_entry(d, payment_entry, do_not_save=False):
|
||||
|
||||
if d.voucher_detail_no:
|
||||
existing_row = payment_entry.get("references", {"name": d["voucher_detail_no"]})[0]
|
||||
|
||||
# Update Advance Paid in SO/PO since they are getting unlinked
|
||||
if existing_row.get("reference_doctype") in ("Sales Order", "Purchase Order"):
|
||||
frappe.get_doc(
|
||||
existing_row.reference_doctype, existing_row.reference_name
|
||||
).set_total_advance_paid()
|
||||
|
||||
original_row = existing_row.as_dict().copy()
|
||||
existing_row.update(reference_details)
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ class Asset(AccountsController):
|
||||
self.validate_item()
|
||||
self.validate_cost_center()
|
||||
self.set_missing_values()
|
||||
self.validate_finance_books()
|
||||
self.prepare_depreciation_data()
|
||||
self.validate_gross_and_purchase_amount()
|
||||
if self.get("schedules"):
|
||||
@@ -197,6 +198,27 @@ class Asset(AccountsController):
|
||||
finance_books = get_item_details(self.item_code, self.asset_category)
|
||||
self.set("finance_books", finance_books)
|
||||
|
||||
def validate_finance_books(self):
|
||||
if not self.calculate_depreciation or len(self.finance_books) == 1:
|
||||
return
|
||||
|
||||
finance_books = set()
|
||||
|
||||
for d in self.finance_books:
|
||||
if d.finance_book in finance_books:
|
||||
frappe.throw(
|
||||
_("Row #{}: Please use a different Finance Book.").format(d.idx),
|
||||
title=_("Duplicate Finance Book"),
|
||||
)
|
||||
else:
|
||||
finance_books.add(d.finance_book)
|
||||
|
||||
if not d.finance_book:
|
||||
frappe.throw(
|
||||
_("Row #{}: Finance Book should not be empty since you're using multiple.").format(d.idx),
|
||||
title=_("Missing Finance Book"),
|
||||
)
|
||||
|
||||
def validate_asset_values(self):
|
||||
if not self.asset_category:
|
||||
self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category")
|
||||
|
||||
@@ -1287,6 +1287,7 @@ class TestDepreciationBasics(AssetSetup):
|
||||
asset.append(
|
||||
"finance_books",
|
||||
{
|
||||
"finance_book": "Test Finance Book 1",
|
||||
"depreciation_method": "Straight Line",
|
||||
"frequency_of_depreciation": 1,
|
||||
"total_number_of_depreciations": 3,
|
||||
@@ -1297,6 +1298,7 @@ class TestDepreciationBasics(AssetSetup):
|
||||
asset.append(
|
||||
"finance_books",
|
||||
{
|
||||
"finance_book": "Test Finance Book 2",
|
||||
"depreciation_method": "Straight Line",
|
||||
"frequency_of_depreciation": 1,
|
||||
"total_number_of_depreciations": 6,
|
||||
@@ -1307,6 +1309,7 @@ class TestDepreciationBasics(AssetSetup):
|
||||
asset.append(
|
||||
"finance_books",
|
||||
{
|
||||
"finance_book": "Test Finance Book 3",
|
||||
"depreciation_method": "Straight Line",
|
||||
"frequency_of_depreciation": 12,
|
||||
"total_number_of_depreciations": 3,
|
||||
@@ -1336,6 +1339,7 @@ class TestDepreciationBasics(AssetSetup):
|
||||
asset.append(
|
||||
"finance_books",
|
||||
{
|
||||
"finance_book": "Test Finance Book 1",
|
||||
"depreciation_method": "Straight Line",
|
||||
"frequency_of_depreciation": 12,
|
||||
"total_number_of_depreciations": 3,
|
||||
@@ -1346,6 +1350,7 @@ class TestDepreciationBasics(AssetSetup):
|
||||
asset.append(
|
||||
"finance_books",
|
||||
{
|
||||
"finance_book": "Test Finance Book 2",
|
||||
"depreciation_method": "Straight Line",
|
||||
"frequency_of_depreciation": 12,
|
||||
"total_number_of_depreciations": 6,
|
||||
@@ -1581,6 +1586,15 @@ def create_asset_data():
|
||||
if not frappe.db.exists("Location", "Test Location"):
|
||||
frappe.get_doc({"doctype": "Location", "location_name": "Test Location"}).insert()
|
||||
|
||||
if not frappe.db.exists("Finance Book", "Test Finance Book 1"):
|
||||
frappe.get_doc({"doctype": "Finance Book", "finance_book_name": "Test Finance Book 1"}).insert()
|
||||
|
||||
if not frappe.db.exists("Finance Book", "Test Finance Book 2"):
|
||||
frappe.get_doc({"doctype": "Finance Book", "finance_book_name": "Test Finance Book 2"}).insert()
|
||||
|
||||
if not frappe.db.exists("Finance Book", "Test Finance Book 3"):
|
||||
frappe.get_doc({"doctype": "Finance Book", "finance_book_name": "Test Finance Book 3"}).insert()
|
||||
|
||||
|
||||
def create_asset(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -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)",
|
||||
)
|
||||
|
||||
|
||||
@@ -6,7 +6,12 @@
|
||||
frappe.ui.form.on('Loan Repayment', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
// },
|
||||
|
||||
setup: function(frm) {
|
||||
frm.add_fetch("against_loan", "repay_from_salary", "repay_from_salary");
|
||||
},
|
||||
|
||||
onload: function(frm) {
|
||||
frm.set_query('against_loan', function() {
|
||||
return {
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
"posting_date",
|
||||
"clearance_date",
|
||||
"rate_of_interest",
|
||||
"payroll_payable_account",
|
||||
"is_term_loan",
|
||||
"repay_from_salary",
|
||||
"payment_details_section",
|
||||
@@ -41,6 +40,7 @@
|
||||
"amended_from",
|
||||
"accounting_details_section",
|
||||
"payment_account",
|
||||
"payroll_payable_account",
|
||||
"penalty_income_account",
|
||||
"column_break_36",
|
||||
"loan_account"
|
||||
@@ -262,7 +262,6 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "against_loan.repay_from_salary",
|
||||
"fieldname": "repay_from_salary",
|
||||
"fieldtype": "Check",
|
||||
"label": "Repay From Salary"
|
||||
@@ -280,6 +279,7 @@
|
||||
"label": "Accounting Details"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.repay_from_salary",
|
||||
"fetch_from": "against_loan.payment_account",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "payment_account",
|
||||
@@ -311,11 +311,10 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-08-04 17:13:51.964203",
|
||||
"modified": "2023-09-18 16:50:32.897005",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan Repayment",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -351,6 +350,5 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
@@ -81,6 +81,11 @@ class LoanRepayment(AccountsController):
|
||||
if amounts.get("due_date"):
|
||||
self.due_date = amounts.get("due_date")
|
||||
|
||||
if self.repay_from_salary and not self.payroll_payable_account:
|
||||
frappe.throw(_("Please set Payroll Payable Account in Loan Repayment"))
|
||||
elif not self.repay_from_salary and self.payroll_payable_account:
|
||||
self.repay_from_salary = 1
|
||||
|
||||
def check_future_entries(self):
|
||||
future_repayment_date = frappe.db.get_value(
|
||||
"Loan Repayment",
|
||||
|
||||
@@ -377,3 +377,5 @@ execute:frappe.db.set_value("Naming Series", "Naming Series", {"select_doc_for_s
|
||||
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)
|
||||
|
||||
119
erpnext/patches/v13_0/correct_asset_value_if_je_with_workflow.py
Normal file
119
erpnext/patches/v13_0/correct_asset_value_if_je_with_workflow.py
Normal file
@@ -0,0 +1,119 @@
|
||||
import frappe
|
||||
from frappe.model.workflow import get_workflow_name
|
||||
from frappe.query_builder.functions import IfNull, Sum
|
||||
|
||||
|
||||
def execute():
|
||||
active_je_workflow = get_workflow_name("Journal Entry")
|
||||
if not active_je_workflow:
|
||||
return
|
||||
|
||||
correct_value_for_assets_with_manual_depr_entries()
|
||||
|
||||
finance_books = frappe.db.get_all("Finance Book", pluck="name")
|
||||
|
||||
if finance_books:
|
||||
for fb_name in finance_books:
|
||||
correct_value_for_assets_with_auto_depr(fb_name)
|
||||
|
||||
correct_value_for_assets_with_auto_depr()
|
||||
|
||||
|
||||
def correct_value_for_assets_with_manual_depr_entries():
|
||||
asset = frappe.qb.DocType("Asset")
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
aca = frappe.qb.DocType("Asset Category Account")
|
||||
company = frappe.qb.DocType("Company")
|
||||
|
||||
asset_details_and_depr_amount_map = (
|
||||
frappe.qb.from_(gle)
|
||||
.join(asset)
|
||||
.on(gle.against_voucher == asset.name)
|
||||
.join(aca)
|
||||
.on((aca.parent == asset.asset_category) & (aca.company_name == asset.company))
|
||||
.join(company)
|
||||
.on(company.name == asset.company)
|
||||
.select(
|
||||
asset.name.as_("asset_name"),
|
||||
asset.gross_purchase_amount.as_("gross_purchase_amount"),
|
||||
asset.opening_accumulated_depreciation.as_("opening_accumulated_depreciation"),
|
||||
Sum(gle.debit).as_("depr_amount"),
|
||||
)
|
||||
.where(
|
||||
gle.account == IfNull(aca.depreciation_expense_account, company.depreciation_expense_account)
|
||||
)
|
||||
.where(gle.debit != 0)
|
||||
.where(gle.is_cancelled == 0)
|
||||
.where(asset.docstatus == 1)
|
||||
.where(asset.calculate_depreciation == 0)
|
||||
.groupby(asset.name)
|
||||
)
|
||||
|
||||
frappe.qb.update(asset).join(asset_details_and_depr_amount_map).on(
|
||||
asset_details_and_depr_amount_map.asset_name == asset.name
|
||||
).set(
|
||||
asset.value_after_depreciation,
|
||||
asset_details_and_depr_amount_map.gross_purchase_amount
|
||||
- asset_details_and_depr_amount_map.opening_accumulated_depreciation
|
||||
- asset_details_and_depr_amount_map.depr_amount,
|
||||
).run()
|
||||
|
||||
|
||||
def correct_value_for_assets_with_auto_depr(fb_name=None):
|
||||
asset = frappe.qb.DocType("Asset")
|
||||
gle = frappe.qb.DocType("GL Entry")
|
||||
aca = frappe.qb.DocType("Asset Category Account")
|
||||
company = frappe.qb.DocType("Company")
|
||||
afb = frappe.qb.DocType("Asset Finance Book")
|
||||
|
||||
asset_details_and_depr_amount_map = (
|
||||
frappe.qb.from_(gle)
|
||||
.join(asset)
|
||||
.on(gle.against_voucher == asset.name)
|
||||
.join(aca)
|
||||
.on((aca.parent == asset.asset_category) & (aca.company_name == asset.company))
|
||||
.join(company)
|
||||
.on(company.name == asset.company)
|
||||
.select(
|
||||
asset.name.as_("asset_name"),
|
||||
asset.gross_purchase_amount.as_("gross_purchase_amount"),
|
||||
asset.opening_accumulated_depreciation.as_("opening_accumulated_depreciation"),
|
||||
Sum(gle.debit).as_("depr_amount"),
|
||||
)
|
||||
.where(
|
||||
gle.account == IfNull(aca.depreciation_expense_account, company.depreciation_expense_account)
|
||||
)
|
||||
.where(gle.debit != 0)
|
||||
.where(gle.is_cancelled == 0)
|
||||
.where(asset.docstatus == 1)
|
||||
.where(asset.calculate_depreciation == 1)
|
||||
.groupby(asset.name)
|
||||
)
|
||||
|
||||
if fb_name:
|
||||
asset_details_and_depr_amount_map = asset_details_and_depr_amount_map.where(
|
||||
gle.finance_book == fb_name
|
||||
)
|
||||
else:
|
||||
asset_details_and_depr_amount_map = asset_details_and_depr_amount_map.where(
|
||||
(gle.finance_book.isin([""])) | (gle.finance_book.isnull())
|
||||
)
|
||||
|
||||
query = (
|
||||
frappe.qb.update(afb)
|
||||
.join(asset_details_and_depr_amount_map)
|
||||
.on(asset_details_and_depr_amount_map.asset_name == afb.parent)
|
||||
.set(
|
||||
afb.value_after_depreciation,
|
||||
asset_details_and_depr_amount_map.gross_purchase_amount
|
||||
- asset_details_and_depr_amount_map.opening_accumulated_depreciation
|
||||
- asset_details_and_depr_amount_map.depr_amount,
|
||||
)
|
||||
)
|
||||
|
||||
if fb_name:
|
||||
query = query.where(afb.finance_book == fb_name)
|
||||
else:
|
||||
query = query.where((afb.finance_book.isin([""])) | (afb.finance_book.isnull()))
|
||||
|
||||
query.run()
|
||||
@@ -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)) {
|
||||
|
||||
@@ -6,8 +6,10 @@ erpnext.financial_statements = {
|
||||
if (data && column.fieldname=="account") {
|
||||
value = data.account_name || value;
|
||||
|
||||
column.link_onclick =
|
||||
"erpnext.financial_statements.open_general_ledger(" + JSON.stringify(data) + ")";
|
||||
if (data.account) {
|
||||
column.link_onclick =
|
||||
"erpnext.financial_statements.open_general_ledger(" + JSON.stringify(data) + ")";
|
||||
}
|
||||
column.is_tree = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -96,18 +96,26 @@ class SalesOrder(SellingController):
|
||||
and customer = %s",
|
||||
(self.po_no, self.name, self.customer),
|
||||
)
|
||||
if (
|
||||
so
|
||||
and so[0][0]
|
||||
and not cint(
|
||||
if so and so[0][0]:
|
||||
if cint(
|
||||
frappe.db.get_single_value("Selling Settings", "allow_against_multiple_purchase_orders")
|
||||
)
|
||||
):
|
||||
frappe.msgprint(
|
||||
_("Warning: Sales Order {0} already exists against Customer's Purchase Order {1}").format(
|
||||
so[0][0], self.po_no
|
||||
):
|
||||
frappe.msgprint(
|
||||
_("Warning: Sales Order {0} already exists against Customer's Purchase Order {1}").format(
|
||||
frappe.bold(so[0][0]), frappe.bold(self.po_no)
|
||||
)
|
||||
)
|
||||
else:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Sales Order {0} already exists against Customer's Purchase Order {1}. To allow multiple Sales Orders, Enable {2} in {3}"
|
||||
).format(
|
||||
frappe.bold(so[0][0]),
|
||||
frappe.bold(self.po_no),
|
||||
frappe.bold(_("'Allow Multiple Sales Orders Against a Customer's Purchase Order'")),
|
||||
get_link_to_form("Selling Settings", "Selling Settings"),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def validate_for_items(self):
|
||||
for d in self.get("items"):
|
||||
|
||||
@@ -1741,7 +1741,7 @@ def make_sales_order(**args):
|
||||
so.company = args.company or "_Test Company"
|
||||
so.customer = args.customer or "_Test Customer"
|
||||
so.currency = args.currency or "INR"
|
||||
so.po_no = args.po_no or "12345"
|
||||
so.po_no = args.po_no or ""
|
||||
if args.selling_price_list:
|
||||
so.selling_price_list = args.selling_price_list
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1268,6 +1268,7 @@
|
||||
"depends_on": "eval: doc.is_internal_customer",
|
||||
"fieldname": "set_target_warehouse",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Set Target Warehouse",
|
||||
"no_copy": 1,
|
||||
@@ -1335,7 +1336,7 @@
|
||||
"idx": 146,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-09-16 17:46:17.701904",
|
||||
"modified": "2023-09-04 14:15:28.363184",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Delivery Note",
|
||||
@@ -1405,4 +1406,4 @@
|
||||
"title_field": "title",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -697,7 +697,7 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
|
||||
def test_dn_billing_status_case1(self):
|
||||
# SO -> DN -> SI
|
||||
so = make_sales_order()
|
||||
so = make_sales_order(po_no="12345")
|
||||
dn = create_dn_against_so(so.name, delivered_qty=2)
|
||||
|
||||
self.assertEqual(dn.status, "To Bill")
|
||||
@@ -724,7 +724,7 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
make_sales_invoice,
|
||||
)
|
||||
|
||||
so = make_sales_order()
|
||||
so = make_sales_order(po_no="12345")
|
||||
|
||||
si = make_sales_invoice(so.name)
|
||||
si.get("items")[0].qty = 5
|
||||
@@ -768,7 +768,7 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
|
||||
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
|
||||
|
||||
so = make_sales_order()
|
||||
so = make_sales_order(po_no="12345")
|
||||
|
||||
dn1 = make_delivery_note(so.name)
|
||||
dn1.get("items")[0].qty = 2
|
||||
@@ -814,7 +814,7 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_delivery_note
|
||||
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
|
||||
|
||||
so = make_sales_order()
|
||||
so = make_sales_order(po_no="12345")
|
||||
|
||||
si = make_sales_invoice(so.name)
|
||||
si.submit()
|
||||
@@ -1180,6 +1180,25 @@ class TestDeliveryNote(FrappeTestCase):
|
||||
|
||||
self.assertTrue(return_dn.docstatus == 1)
|
||||
|
||||
def tearDown(self):
|
||||
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"):
|
||||
|
||||
@@ -33,5 +33,43 @@ frappe.query_reports["Stock and Account Value Comparison"] = {
|
||||
"fieldtype": "Date",
|
||||
"default": frappe.datetime.get_today(),
|
||||
},
|
||||
]
|
||||
],
|
||||
|
||||
get_datatable_options(options) {
|
||||
return Object.assign(options, {
|
||||
checkboxColumn: true,
|
||||
});
|
||||
},
|
||||
|
||||
onload(report) {
|
||||
report.page.add_inner_button(__("Create Reposting Entries"), function() {
|
||||
let message = `
|
||||
<div>
|
||||
<p>
|
||||
Reposting Entries will change the value of
|
||||
accounts Stock In Hand, and Stock Expenses
|
||||
in the Trial Balance report and will also change
|
||||
the Balance Value in the Stock Balance report.
|
||||
</p>
|
||||
<p>Are you sure you want to create Reposting Entries?</p>
|
||||
</div>`;
|
||||
let indexes = frappe.query_report.datatable.rowmanager.getCheckedRows();
|
||||
let selected_rows = indexes.map(i => frappe.query_report.data[i]);
|
||||
|
||||
if (!selected_rows.length) {
|
||||
frappe.throw(__("Please select rows to create Reposting Entries"));
|
||||
}
|
||||
|
||||
frappe.confirm(__(message), () => {
|
||||
frappe.call({
|
||||
method: "erpnext.stock.report.stock_and_account_value_comparison.stock_and_account_value_comparison.create_reposting_entries",
|
||||
args: {
|
||||
rows: selected_rows,
|
||||
company: frappe.query_report.get_filter_values().company
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import get_link_to_form, parse_json
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.utils import get_currency_precision, get_stock_accounts
|
||||
@@ -134,3 +135,35 @@ def get_columns(filters):
|
||||
"width": "120",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_reposting_entries(rows, company):
|
||||
if isinstance(rows, str):
|
||||
rows = parse_json(rows)
|
||||
|
||||
entries = []
|
||||
for row in rows:
|
||||
row = frappe._dict(row)
|
||||
|
||||
try:
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Repost Item Valuation",
|
||||
"based_on": "Transaction",
|
||||
"status": "Queued",
|
||||
"voucher_type": row.voucher_type,
|
||||
"voucher_no": row.voucher_no,
|
||||
"posting_date": row.posting_date,
|
||||
"company": company,
|
||||
"allow_nagative_stock": 1,
|
||||
}
|
||||
).submit()
|
||||
|
||||
entries.append(get_link_to_form("Repost Item Valuation", doc.name))
|
||||
except frappe.DuplicateEntryError:
|
||||
pass
|
||||
|
||||
if entries:
|
||||
entries = ", ".join(entries)
|
||||
frappe.msgprint(_("Reposting entries created: {0}").format(entries))
|
||||
|
||||
@@ -3,23 +3,23 @@
|
||||
/* eslint-disable */
|
||||
|
||||
const DIFFERNCE_FIELD_NAMES = [
|
||||
"difference_in_qty",
|
||||
"fifo_qty_diff",
|
||||
"fifo_value_diff",
|
||||
"fifo_valuation_diff",
|
||||
"valuation_diff",
|
||||
"fifo_difference_diff",
|
||||
"diff_value_diff"
|
||||
'difference_in_qty',
|
||||
'fifo_qty_diff',
|
||||
'fifo_value_diff',
|
||||
'fifo_valuation_diff',
|
||||
'valuation_diff',
|
||||
'fifo_difference_diff',
|
||||
'diff_value_diff'
|
||||
];
|
||||
|
||||
frappe.query_reports["Stock Ledger Invariant Check"] = {
|
||||
"filters": [
|
||||
frappe.query_reports['Stock Ledger Invariant Check'] = {
|
||||
'filters': [
|
||||
{
|
||||
"fieldname": "item_code",
|
||||
"fieldtype": "Link",
|
||||
"label": "Item",
|
||||
"mandatory": 1,
|
||||
"options": "Item",
|
||||
'fieldname': 'item_code',
|
||||
'fieldtype': 'Link',
|
||||
'label': 'Item',
|
||||
'mandatory': 1,
|
||||
'options': 'Item',
|
||||
get_query: function() {
|
||||
return {
|
||||
filters: {is_stock_item: 1, has_serial_no: 0}
|
||||
@@ -27,18 +27,61 @@ frappe.query_reports["Stock Ledger Invariant Check"] = {
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname": "warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": "Warehouse",
|
||||
"mandatory": 1,
|
||||
"options": "Warehouse",
|
||||
'fieldname': 'warehouse',
|
||||
'fieldtype': 'Link',
|
||||
'label': 'Warehouse',
|
||||
'mandatory': 1,
|
||||
'options': 'Warehouse',
|
||||
}
|
||||
],
|
||||
|
||||
formatter (value, row, column, data, default_formatter) {
|
||||
value = default_formatter(value, row, column, data);
|
||||
if (DIFFERNCE_FIELD_NAMES.includes(column.fieldname) && Math.abs(data[column.fieldname]) > 0.001) {
|
||||
value = "<span style='color:red'>" + value + "</span>";
|
||||
value = '<span style="color:red">' + value + '</span>';
|
||||
}
|
||||
return value;
|
||||
},
|
||||
|
||||
get_datatable_options(options) {
|
||||
return Object.assign(options, {
|
||||
checkboxColumn: true,
|
||||
});
|
||||
},
|
||||
|
||||
onload(report) {
|
||||
report.page.add_inner_button(__('Create Reposting Entry'), () => {
|
||||
let message = `
|
||||
<div>
|
||||
<p>
|
||||
Reposting Entry will change the value of
|
||||
accounts Stock In Hand, and Stock Expenses
|
||||
in the Trial Balance report and will also change
|
||||
the Balance Value in the Stock Balance report.
|
||||
</p>
|
||||
<p>Are you sure you want to create a Reposting Entry?</p>
|
||||
</div>`;
|
||||
let indexes = frappe.query_report.datatable.rowmanager.getCheckedRows();
|
||||
let selected_rows = indexes.map(i => frappe.query_report.data[i]);
|
||||
|
||||
if (!selected_rows.length) {
|
||||
frappe.throw(__('Please select a row to create a Reposting Entry'));
|
||||
}
|
||||
else if (selected_rows.length > 1) {
|
||||
frappe.throw(__('Please select only one row to create a Reposting Entry'));
|
||||
}
|
||||
else {
|
||||
frappe.confirm(__(message), () => {
|
||||
frappe.call({
|
||||
method: 'erpnext.stock.report.stock_ledger_invariant_check.stock_ledger_invariant_check.create_reposting_entries',
|
||||
args: {
|
||||
rows: selected_rows,
|
||||
item_code: frappe.query_report.get_filter_values().item_code,
|
||||
warehouse: frappe.query_report.get_filter_values().warehouse,
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import get_link_to_form, parse_json
|
||||
|
||||
SLE_FIELDS = (
|
||||
"name",
|
||||
@@ -247,3 +249,35 @@ def get_columns():
|
||||
"label": "H - J",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_reposting_entries(rows, item_code=None, warehouse=None):
|
||||
if isinstance(rows, str):
|
||||
rows = parse_json(rows)
|
||||
|
||||
entries = []
|
||||
for row in rows:
|
||||
row = frappe._dict(row)
|
||||
|
||||
try:
|
||||
doc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Repost Item Valuation",
|
||||
"based_on": "Item and Warehouse",
|
||||
"status": "Queued",
|
||||
"item_code": item_code or row.item_code,
|
||||
"warehouse": warehouse or row.warehouse,
|
||||
"posting_date": row.posting_date,
|
||||
"posting_time": row.posting_time,
|
||||
"allow_nagative_stock": 1,
|
||||
}
|
||||
).submit()
|
||||
|
||||
entries.append(get_link_to_form("Repost Item Valuation", doc.name))
|
||||
except frappe.DuplicateEntryError:
|
||||
continue
|
||||
|
||||
if entries:
|
||||
entries = ", ".join(entries)
|
||||
frappe.msgprint(_("Reposting entries created: {0}").format(entries))
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
const DIFFERENCE_FIELD_NAMES = [
|
||||
"difference_in_qty",
|
||||
"fifo_qty_diff",
|
||||
"fifo_value_diff",
|
||||
"fifo_valuation_diff",
|
||||
"valuation_diff",
|
||||
"fifo_difference_diff",
|
||||
"diff_value_diff"
|
||||
];
|
||||
|
||||
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"),
|
||||
"options": "Item",
|
||||
get_query: function() {
|
||||
return {
|
||||
filters: {is_stock_item: 1, has_serial_no: 0}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname": "warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": __("Warehouse"),
|
||||
"options": "Warehouse",
|
||||
get_query: function() {
|
||||
return {
|
||||
filters: {is_group: 0, disabled: 0}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname": "difference_in",
|
||||
"fieldtype": "Select",
|
||||
"label": __("Difference In"),
|
||||
"options": [
|
||||
"",
|
||||
"Qty",
|
||||
"Value",
|
||||
"Valuation",
|
||||
],
|
||||
},
|
||||
{
|
||||
"fieldname": "include_disabled",
|
||||
"fieldtype": "Check",
|
||||
"label": __("Include Disabled"),
|
||||
}
|
||||
],
|
||||
|
||||
formatter (value, row, column, data, default_formatter) {
|
||||
value = default_formatter(value, row, column, data);
|
||||
|
||||
if (DIFFERENCE_FIELD_NAMES.includes(column.fieldname) && Math.abs(data[column.fieldname]) > 0.001) {
|
||||
value = "<span style='color:red'>" + value + "</span>";
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
|
||||
get_datatable_options(options) {
|
||||
return Object.assign(options, {
|
||||
checkboxColumn: true,
|
||||
});
|
||||
},
|
||||
|
||||
onload(report) {
|
||||
report.page.add_inner_button(__('Create Reposting Entries'), () => {
|
||||
let message = `
|
||||
<div>
|
||||
<p>
|
||||
Reposting Entries will change the value of
|
||||
accounts Stock In Hand, and Stock Expenses
|
||||
in the Trial Balance report and will also change
|
||||
the Balance Value in the Stock Balance report.
|
||||
</p>
|
||||
<p>Are you sure you want to create Reposting Entries?</p>
|
||||
</div>`;
|
||||
let indexes = frappe.query_report.datatable.rowmanager.getCheckedRows();
|
||||
let selected_rows = indexes.map(i => frappe.query_report.data[i]);
|
||||
|
||||
if (!selected_rows.length) {
|
||||
frappe.throw(__("Please select rows to create Reposting Entries"));
|
||||
}
|
||||
|
||||
frappe.confirm(__(message), () => {
|
||||
frappe.call({
|
||||
method: 'erpnext.stock.report.stock_ledger_invariant_check.stock_ledger_invariant_check.create_reposting_entries',
|
||||
args: {
|
||||
rows: selected_rows,
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2023-09-20 10:44:19.414449",
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"letterhead": null,
|
||||
"modified": "2023-09-20 10:44:19.414449",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Ledger Variance",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Stock Ledger Entry",
|
||||
"report_name": "Stock Ledger Variance",
|
||||
"report_type": "Script Report",
|
||||
"roles": []
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import cint, flt
|
||||
|
||||
from erpnext.stock.report.stock_ledger_invariant_check.stock_ledger_invariant_check import (
|
||||
get_data as stock_ledger_invariant_check,
|
||||
)
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
columns, data = [], []
|
||||
|
||||
filters = frappe._dict(filters or {})
|
||||
columns = get_columns()
|
||||
data = get_data(filters)
|
||||
|
||||
return columns, data
|
||||
|
||||
|
||||
def get_columns():
|
||||
return [
|
||||
{
|
||||
"fieldname": "name",
|
||||
"fieldtype": "Link",
|
||||
"label": _("Stock Ledger Entry"),
|
||||
"options": "Stock Ledger Entry",
|
||||
},
|
||||
{
|
||||
"fieldname": "posting_date",
|
||||
"fieldtype": "Data",
|
||||
"label": _("Posting Date"),
|
||||
},
|
||||
{
|
||||
"fieldname": "posting_time",
|
||||
"fieldtype": "Data",
|
||||
"label": _("Posting Time"),
|
||||
},
|
||||
{
|
||||
"fieldname": "creation",
|
||||
"fieldtype": "Data",
|
||||
"label": _("Creation"),
|
||||
},
|
||||
{
|
||||
"fieldname": "item_code",
|
||||
"fieldtype": "Link",
|
||||
"label": _("Item"),
|
||||
"options": "Item",
|
||||
},
|
||||
{
|
||||
"fieldname": "warehouse",
|
||||
"fieldtype": "Link",
|
||||
"label": _("Warehouse"),
|
||||
"options": "Warehouse",
|
||||
},
|
||||
{
|
||||
"fieldname": "valuation_method",
|
||||
"fieldtype": "Data",
|
||||
"label": _("Valuation Method"),
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_type",
|
||||
"fieldtype": "Link",
|
||||
"label": _("Voucher Type"),
|
||||
"options": "DocType",
|
||||
},
|
||||
{
|
||||
"fieldname": "voucher_no",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": _("Voucher No"),
|
||||
"options": "voucher_type",
|
||||
},
|
||||
{
|
||||
"fieldname": "batch_no",
|
||||
"fieldtype": "Link",
|
||||
"label": _("Batch"),
|
||||
"options": "Batch",
|
||||
},
|
||||
{
|
||||
"fieldname": "use_batchwise_valuation",
|
||||
"fieldtype": "Check",
|
||||
"label": _("Batchwise Valuation"),
|
||||
},
|
||||
{
|
||||
"fieldname": "actual_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": _("Qty Change"),
|
||||
},
|
||||
{
|
||||
"fieldname": "incoming_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": _("Incoming Rate"),
|
||||
},
|
||||
{
|
||||
"fieldname": "consumption_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": _("Consumption Rate"),
|
||||
},
|
||||
{
|
||||
"fieldname": "qty_after_transaction",
|
||||
"fieldtype": "Float",
|
||||
"label": _("(A) Qty After Transaction"),
|
||||
},
|
||||
{
|
||||
"fieldname": "expected_qty_after_transaction",
|
||||
"fieldtype": "Float",
|
||||
"label": _("(B) Expected Qty After Transaction"),
|
||||
},
|
||||
{
|
||||
"fieldname": "difference_in_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": _("A - B"),
|
||||
},
|
||||
{
|
||||
"fieldname": "stock_queue",
|
||||
"fieldtype": "Data",
|
||||
"label": _("FIFO/LIFO Queue"),
|
||||
},
|
||||
{
|
||||
"fieldname": "fifo_queue_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": _("(C) Total Qty in Queue"),
|
||||
},
|
||||
{
|
||||
"fieldname": "fifo_qty_diff",
|
||||
"fieldtype": "Float",
|
||||
"label": _("A - C"),
|
||||
},
|
||||
{
|
||||
"fieldname": "stock_value",
|
||||
"fieldtype": "Float",
|
||||
"label": _("(D) Balance Stock Value"),
|
||||
},
|
||||
{
|
||||
"fieldname": "fifo_stock_value",
|
||||
"fieldtype": "Float",
|
||||
"label": _("(E) Balance Stock Value in Queue"),
|
||||
},
|
||||
{
|
||||
"fieldname": "fifo_value_diff",
|
||||
"fieldtype": "Float",
|
||||
"label": _("D - E"),
|
||||
},
|
||||
{
|
||||
"fieldname": "stock_value_difference",
|
||||
"fieldtype": "Float",
|
||||
"label": _("(F) Change in Stock Value"),
|
||||
},
|
||||
{
|
||||
"fieldname": "stock_value_from_diff",
|
||||
"fieldtype": "Float",
|
||||
"label": _("(G) Sum of Change in Stock Value"),
|
||||
},
|
||||
{
|
||||
"fieldname": "diff_value_diff",
|
||||
"fieldtype": "Float",
|
||||
"label": _("G - D"),
|
||||
},
|
||||
{
|
||||
"fieldname": "fifo_stock_diff",
|
||||
"fieldtype": "Float",
|
||||
"label": _("(H) Change in Stock Value (FIFO Queue)"),
|
||||
},
|
||||
{
|
||||
"fieldname": "fifo_difference_diff",
|
||||
"fieldtype": "Float",
|
||||
"label": _("H - F"),
|
||||
},
|
||||
{
|
||||
"fieldname": "valuation_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": _("(I) Valuation Rate"),
|
||||
},
|
||||
{
|
||||
"fieldname": "fifo_valuation_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": _("(J) Valuation Rate as per FIFO"),
|
||||
},
|
||||
{
|
||||
"fieldname": "fifo_valuation_diff",
|
||||
"fieldtype": "Float",
|
||||
"label": _("I - J"),
|
||||
},
|
||||
{
|
||||
"fieldname": "balance_value_by_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": _("(K) Valuation = Value (D) ÷ Qty (A)"),
|
||||
},
|
||||
{
|
||||
"fieldname": "valuation_diff",
|
||||
"fieldtype": "Float",
|
||||
"label": _("I - K"),
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
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:
|
||||
precision = cint(frappe.db.get_single_value("System Settings", "float_precision"))
|
||||
|
||||
for item_warehouse in item_warehouse_map:
|
||||
report_data = stock_ledger_invariant_check(item_warehouse)
|
||||
|
||||
if not report_data:
|
||||
continue
|
||||
|
||||
for row in report_data:
|
||||
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
|
||||
|
||||
|
||||
def get_item_warehouse_combinations(filters: dict = None) -> dict:
|
||||
filters = frappe._dict(filters or {})
|
||||
|
||||
bin = frappe.qb.DocType("Bin")
|
||||
item = frappe.qb.DocType("Item")
|
||||
warehouse = frappe.qb.DocType("Warehouse")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(bin)
|
||||
.inner_join(item)
|
||||
.on(bin.item_code == item.name)
|
||||
.inner_join(warehouse)
|
||||
.on(bin.warehouse == warehouse.name)
|
||||
.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)
|
||||
)
|
||||
)
|
||||
|
||||
if filters.item_code:
|
||||
query = query.where(item.name == filters.item_code)
|
||||
if filters.warehouse:
|
||||
query = query.where(warehouse.name == filters.warehouse)
|
||||
if not filters.include_disabled:
|
||||
query = query.where((item.disabled == 0) & (warehouse.disabled == 0))
|
||||
|
||||
return query.run(as_dict=1)
|
||||
|
||||
|
||||
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 qty_diff:
|
||||
return True
|
||||
elif difference_in == "Value" and value_diff:
|
||||
return True
|
||||
elif difference_in == "Valuation" and valuation_diff:
|
||||
return True
|
||||
elif difference_in not in ["Qty", "Value", "Valuation"] and (
|
||||
qty_diff or value_diff or valuation_diff
|
||||
):
|
||||
return True
|
||||
@@ -503,7 +503,7 @@ class update_entries_after(object):
|
||||
|
||||
def update_distinct_item_warehouses(self, dependant_sle):
|
||||
key = (dependant_sle.item_code, dependant_sle.warehouse)
|
||||
val = frappe._dict({"sle": dependant_sle, "dependent_voucher_detail_nos": []})
|
||||
val = frappe._dict({"sle": dependant_sle})
|
||||
|
||||
if key not in self.distinct_item_warehouses:
|
||||
self.distinct_item_warehouses[key] = val
|
||||
@@ -517,6 +517,8 @@ class update_entries_after(object):
|
||||
|
||||
if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
|
||||
val.sle_changed = True
|
||||
dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no)
|
||||
val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
|
||||
self.distinct_item_warehouses[key] = val
|
||||
self.new_items_found = True
|
||||
elif dependant_sle.voucher_detail_no not in set(dependent_voucher_detail_nos):
|
||||
@@ -656,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