Merge remote-tracking branch 'upstream/develop' into feat/so-po-advance-payment-status
This commit is contained in:
commit
c78fdaae90
16
.github/helper/install.sh
vendored
16
.github/helper/install.sh
vendored
@ -4,7 +4,9 @@ set -e
|
|||||||
|
|
||||||
cd ~ || exit
|
cd ~ || exit
|
||||||
|
|
||||||
sudo apt update && sudo apt install redis-server libcups2-dev
|
sudo apt update
|
||||||
|
sudo apt remove mysql-server mysql-client
|
||||||
|
sudo apt install libcups2-dev redis-server mariadb-client-10.6
|
||||||
|
|
||||||
pip install frappe-bench
|
pip install frappe-bench
|
||||||
|
|
||||||
@ -25,14 +27,14 @@ fi
|
|||||||
|
|
||||||
|
|
||||||
if [ "$DB" == "mariadb" ];then
|
if [ "$DB" == "mariadb" ];then
|
||||||
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'"
|
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'"
|
||||||
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
|
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
|
||||||
|
|
||||||
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'"
|
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'"
|
||||||
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE DATABASE test_frappe"
|
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE DATABASE test_frappe"
|
||||||
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'"
|
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'"
|
||||||
|
|
||||||
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "FLUSH PRIVILEGES"
|
mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "FLUSH PRIVILEGES"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$DB" == "postgres" ];then
|
if [ "$DB" == "postgres" ];then
|
||||||
|
22
.github/workflows/initiate_release.yml
vendored
22
.github/workflows/initiate_release.yml
vendored
@ -15,7 +15,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
version: ["13", "14"]
|
version: ["13", "14", "15"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: octokit/request-action@v2.x
|
- uses: octokit/request-action@v2.x
|
||||||
@ -30,23 +30,3 @@ jobs:
|
|||||||
head: version-${{ matrix.version }}-hotfix
|
head: version-${{ matrix.version }}-hotfix
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
|
||||||
beta-release:
|
|
||||||
name: Release
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: octokit/request-action@v2.x
|
|
||||||
with:
|
|
||||||
route: POST /repos/{owner}/{repo}/pulls
|
|
||||||
owner: frappe
|
|
||||||
repo: erpnext
|
|
||||||
title: |-
|
|
||||||
"chore: release v15 beta"
|
|
||||||
body: "Automated beta release."
|
|
||||||
base: version-15-beta
|
|
||||||
head: develop
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
|
||||||
|
3
.github/workflows/patch.yml
vendored
3
.github/workflows/patch.yml
vendored
@ -28,7 +28,7 @@ jobs:
|
|||||||
MARIADB_ROOT_PASSWORD: 'root'
|
MARIADB_ROOT_PASSWORD: 'root'
|
||||||
ports:
|
ports:
|
||||||
- 3306:3306
|
- 3306:3306
|
||||||
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
|
options: --health-cmd="mariadb-admin ping" --health-interval=5s --health-timeout=2s --health-retries=3
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone
|
- name: Clone
|
||||||
@ -134,6 +134,7 @@ jobs:
|
|||||||
}
|
}
|
||||||
|
|
||||||
update_to_version 14
|
update_to_version 14
|
||||||
|
update_to_version 15
|
||||||
|
|
||||||
echo "Updating to latest version"
|
echo "Updating to latest version"
|
||||||
git -C "apps/frappe" checkout -q -f "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}"
|
git -C "apps/frappe" checkout -q -f "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}"
|
||||||
|
2
.github/workflows/server-tests-mariadb.yml
vendored
2
.github/workflows/server-tests-mariadb.yml
vendored
@ -47,7 +47,7 @@ jobs:
|
|||||||
MARIADB_ROOT_PASSWORD: 'root'
|
MARIADB_ROOT_PASSWORD: 'root'
|
||||||
ports:
|
ports:
|
||||||
- 3306:3306
|
- 3306:3306
|
||||||
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
|
options: --health-cmd="mariadb-admin ping" --health-interval=5s --health-timeout=2s --health-retries=3
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Clone
|
- name: Clone
|
||||||
|
46
.mergify.yml
46
.mergify.yml
@ -17,6 +17,7 @@ pull_request_rules:
|
|||||||
- base=version-12
|
- base=version-12
|
||||||
- base=version-14
|
- base=version-14
|
||||||
- base=version-15
|
- base=version-15
|
||||||
|
- base=version-16
|
||||||
actions:
|
actions:
|
||||||
close:
|
close:
|
||||||
comment:
|
comment:
|
||||||
@ -24,16 +25,6 @@ pull_request_rules:
|
|||||||
@{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch.
|
@{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch.
|
||||||
https://github.com/frappe/erpnext/wiki/Pull-Request-Checklist#which-branch
|
https://github.com/frappe/erpnext/wiki/Pull-Request-Checklist#which-branch
|
||||||
|
|
||||||
- name: Auto-close PRs on pre-release branch
|
|
||||||
conditions:
|
|
||||||
- base=version-13-pre-release
|
|
||||||
actions:
|
|
||||||
close:
|
|
||||||
comment:
|
|
||||||
message: |
|
|
||||||
@{{author}}, pre-release branch is not maintained anymore. Releases are directly done by merging hotfix branch to stable branches.
|
|
||||||
|
|
||||||
|
|
||||||
- name: backport to develop
|
- name: backport to develop
|
||||||
conditions:
|
conditions:
|
||||||
- label="backport develop"
|
- label="backport develop"
|
||||||
@ -54,13 +45,13 @@ pull_request_rules:
|
|||||||
assignees:
|
assignees:
|
||||||
- "{{ author }}"
|
- "{{ author }}"
|
||||||
|
|
||||||
- name: backport to version-14-pre-release
|
- name: backport to version-15-hotfix
|
||||||
conditions:
|
conditions:
|
||||||
- label="backport version-14-pre-release"
|
- label="backport version-15-hotfix"
|
||||||
actions:
|
actions:
|
||||||
backport:
|
backport:
|
||||||
branches:
|
branches:
|
||||||
- version-14-pre-release
|
- version-15-hotfix
|
||||||
assignees:
|
assignees:
|
||||||
- "{{ author }}"
|
- "{{ author }}"
|
||||||
|
|
||||||
@ -74,35 +65,6 @@ pull_request_rules:
|
|||||||
assignees:
|
assignees:
|
||||||
- "{{ author }}"
|
- "{{ author }}"
|
||||||
|
|
||||||
- name: backport to version-13-pre-release
|
|
||||||
conditions:
|
|
||||||
- label="backport version-13-pre-release"
|
|
||||||
actions:
|
|
||||||
backport:
|
|
||||||
branches:
|
|
||||||
- version-13-pre-release
|
|
||||||
assignees:
|
|
||||||
- "{{ author }}"
|
|
||||||
|
|
||||||
- name: backport to version-12-hotfix
|
|
||||||
conditions:
|
|
||||||
- label="backport version-12-hotfix"
|
|
||||||
actions:
|
|
||||||
backport:
|
|
||||||
branches:
|
|
||||||
- version-12-hotfix
|
|
||||||
assignees:
|
|
||||||
- "{{ author }}"
|
|
||||||
|
|
||||||
- name: backport to version-12-pre-release
|
|
||||||
conditions:
|
|
||||||
- label="backport version-12-pre-release"
|
|
||||||
actions:
|
|
||||||
backport:
|
|
||||||
branches:
|
|
||||||
- version-12-pre-release
|
|
||||||
assignees:
|
|
||||||
- "{{ author }}"
|
|
||||||
|
|
||||||
- name: Automatic merge on CI success and review
|
- name: Automatic merge on CI success and review
|
||||||
conditions:
|
conditions:
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"country_code": "ni",
|
"country_code": "ni",
|
||||||
"name": "Nicaragua - Catalogo de Cuentas",
|
"name": "Nicaragua - Catálogo de Cuentas",
|
||||||
"tree": {
|
"tree": {
|
||||||
"Activo": {
|
"Activo": {
|
||||||
"Activo Corriente": {
|
"Activo Corriente": {
|
||||||
@ -491,4 +491,4 @@
|
|||||||
"root_type": "Liability"
|
"root_type": "Liability"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,7 @@ def make_closing_entries(closing_entries, voucher_name, company, closing_date):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
cle.flags.ignore_permissions = True
|
cle.flags.ignore_permissions = True
|
||||||
|
cle.flags.ignore_links = True
|
||||||
cle.submit()
|
cle.submit()
|
||||||
|
|
||||||
|
|
||||||
|
@ -302,3 +302,30 @@ def get_dimensions(with_cost_center_and_project=False):
|
|||||||
default_dimensions_map[dimension.company][dimension.fieldname] = dimension.default_dimension
|
default_dimensions_map[dimension.company][dimension.fieldname] = dimension.default_dimension
|
||||||
|
|
||||||
return dimension_filters, default_dimensions_map
|
return dimension_filters, default_dimensions_map
|
||||||
|
|
||||||
|
|
||||||
|
def create_accounting_dimensions_for_doctype(doctype):
|
||||||
|
accounting_dimensions = frappe.db.get_all(
|
||||||
|
"Accounting Dimension", fields=["fieldname", "label", "document_type", "disabled"]
|
||||||
|
)
|
||||||
|
|
||||||
|
if not accounting_dimensions:
|
||||||
|
return
|
||||||
|
|
||||||
|
for d in accounting_dimensions:
|
||||||
|
field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": d.fieldname})
|
||||||
|
|
||||||
|
if field:
|
||||||
|
continue
|
||||||
|
|
||||||
|
df = {
|
||||||
|
"fieldname": d.fieldname,
|
||||||
|
"label": d.label,
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": d.document_type,
|
||||||
|
"insert_after": "accounting_dimensions_section",
|
||||||
|
}
|
||||||
|
|
||||||
|
create_custom_field(doctype, df, ignore_validate=True)
|
||||||
|
|
||||||
|
frappe.clear_cache(doctype=doctype)
|
||||||
|
@ -32,6 +32,7 @@
|
|||||||
"column_break_19",
|
"column_break_19",
|
||||||
"add_taxes_from_item_tax_template",
|
"add_taxes_from_item_tax_template",
|
||||||
"book_tax_discount_loss",
|
"book_tax_discount_loss",
|
||||||
|
"round_row_wise_tax",
|
||||||
"print_settings",
|
"print_settings",
|
||||||
"show_inclusive_tax_in_print",
|
"show_inclusive_tax_in_print",
|
||||||
"show_taxes_as_table_in_print",
|
"show_taxes_as_table_in_print",
|
||||||
@ -414,6 +415,13 @@
|
|||||||
"fieldname": "ignore_account_closing_balance",
|
"fieldname": "ignore_account_closing_balance",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Ignore Account Closing Balance"
|
"label": "Ignore Account Closing Balance"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"description": "Tax Amount will be rounded on a row(items) level",
|
||||||
|
"fieldname": "round_row_wise_tax",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Round Tax Amount Row-wise"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "icon-cog",
|
"icon": "icon-cog",
|
||||||
@ -421,7 +429,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-07-27 15:05:34.000264",
|
"modified": "2023-08-28 00:12:02.740633",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Accounts Settings",
|
"name": "Accounts Settings",
|
||||||
|
@ -18,6 +18,7 @@ from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_s
|
|||||||
get_entries,
|
get_entries,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.utils import get_account_currency, get_balance_on
|
from erpnext.accounts.utils import get_account_currency, get_balance_on
|
||||||
|
from erpnext.setup.utils import get_exchange_rate
|
||||||
|
|
||||||
|
|
||||||
class BankReconciliationTool(Document):
|
class BankReconciliationTool(Document):
|
||||||
@ -130,7 +131,7 @@ def create_journal_entry_bts(
|
|||||||
bank_transaction = frappe.db.get_values(
|
bank_transaction = frappe.db.get_values(
|
||||||
"Bank Transaction",
|
"Bank Transaction",
|
||||||
bank_transaction_name,
|
bank_transaction_name,
|
||||||
fieldname=["name", "deposit", "withdrawal", "bank_account"],
|
fieldname=["name", "deposit", "withdrawal", "bank_account", "currency"],
|
||||||
as_dict=True,
|
as_dict=True,
|
||||||
)[0]
|
)[0]
|
||||||
company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
|
company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
|
||||||
@ -144,29 +145,94 @@ def create_journal_entry_bts(
|
|||||||
)
|
)
|
||||||
|
|
||||||
company = frappe.get_value("Account", company_account, "company")
|
company = frappe.get_value("Account", company_account, "company")
|
||||||
|
company_default_currency = frappe.get_cached_value("Company", company, "default_currency")
|
||||||
|
company_account_currency = frappe.get_cached_value("Account", company_account, "account_currency")
|
||||||
|
second_account_currency = frappe.get_cached_value("Account", second_account, "account_currency")
|
||||||
|
|
||||||
|
# determine if multi-currency Journal or not
|
||||||
|
is_multi_currency = (
|
||||||
|
True
|
||||||
|
if company_default_currency != company_account_currency
|
||||||
|
or company_default_currency != second_account_currency
|
||||||
|
or company_default_currency != bank_transaction.currency
|
||||||
|
else False
|
||||||
|
)
|
||||||
|
|
||||||
accounts = []
|
accounts = []
|
||||||
# Multi Currency?
|
second_account_dict = {
|
||||||
accounts.append(
|
"account": second_account,
|
||||||
{
|
"account_currency": second_account_currency,
|
||||||
"account": second_account,
|
"credit_in_account_currency": bank_transaction.deposit,
|
||||||
"credit_in_account_currency": bank_transaction.deposit,
|
"debit_in_account_currency": bank_transaction.withdrawal,
|
||||||
"debit_in_account_currency": bank_transaction.withdrawal,
|
"party_type": party_type,
|
||||||
"party_type": party_type,
|
"party": party,
|
||||||
"party": party,
|
"cost_center": get_default_cost_center(company),
|
||||||
"cost_center": get_default_cost_center(company),
|
}
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
accounts.append(
|
company_account_dict = {
|
||||||
{
|
"account": company_account,
|
||||||
"account": company_account,
|
"account_currency": company_account_currency,
|
||||||
"bank_account": bank_transaction.bank_account,
|
"bank_account": bank_transaction.bank_account,
|
||||||
"credit_in_account_currency": bank_transaction.withdrawal,
|
"credit_in_account_currency": bank_transaction.withdrawal,
|
||||||
"debit_in_account_currency": bank_transaction.deposit,
|
"debit_in_account_currency": bank_transaction.deposit,
|
||||||
"cost_center": get_default_cost_center(company),
|
"cost_center": get_default_cost_center(company),
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
# convert transaction amount to company currency
|
||||||
|
if is_multi_currency:
|
||||||
|
exc_rate = get_exchange_rate(bank_transaction.currency, company_default_currency, posting_date)
|
||||||
|
withdrawal_in_company_currency = flt(exc_rate * abs(bank_transaction.withdrawal))
|
||||||
|
deposit_in_company_currency = flt(exc_rate * abs(bank_transaction.deposit))
|
||||||
|
else:
|
||||||
|
withdrawal_in_company_currency = bank_transaction.withdrawal
|
||||||
|
deposit_in_company_currency = bank_transaction.deposit
|
||||||
|
|
||||||
|
# if second account is of foreign currency, convert and set debit and credit fields.
|
||||||
|
if second_account_currency != company_default_currency:
|
||||||
|
exc_rate = get_exchange_rate(second_account_currency, company_default_currency, posting_date)
|
||||||
|
second_account_dict.update(
|
||||||
|
{
|
||||||
|
"exchange_rate": exc_rate,
|
||||||
|
"credit": deposit_in_company_currency,
|
||||||
|
"debit": withdrawal_in_company_currency,
|
||||||
|
"credit_in_account_currency": flt(deposit_in_company_currency / exc_rate) or 0,
|
||||||
|
"debit_in_account_currency": flt(withdrawal_in_company_currency / exc_rate) or 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
second_account_dict.update(
|
||||||
|
{
|
||||||
|
"exchange_rate": 1,
|
||||||
|
"credit": deposit_in_company_currency,
|
||||||
|
"debit": withdrawal_in_company_currency,
|
||||||
|
"credit_in_account_currency": deposit_in_company_currency,
|
||||||
|
"debit_in_account_currency": withdrawal_in_company_currency,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# if company account is of foreign currency, convert and set debit and credit fields.
|
||||||
|
if company_account_currency != company_default_currency:
|
||||||
|
exc_rate = get_exchange_rate(company_account_currency, company_default_currency, posting_date)
|
||||||
|
company_account_dict.update(
|
||||||
|
{
|
||||||
|
"exchange_rate": exc_rate,
|
||||||
|
"credit": withdrawal_in_company_currency,
|
||||||
|
"debit": deposit_in_company_currency,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
company_account_dict.update(
|
||||||
|
{
|
||||||
|
"exchange_rate": 1,
|
||||||
|
"credit": withdrawal_in_company_currency,
|
||||||
|
"debit": deposit_in_company_currency,
|
||||||
|
"credit_in_account_currency": withdrawal_in_company_currency,
|
||||||
|
"debit_in_account_currency": deposit_in_company_currency,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
accounts.append(second_account_dict)
|
||||||
|
accounts.append(company_account_dict)
|
||||||
|
|
||||||
journal_entry_dict = {
|
journal_entry_dict = {
|
||||||
"voucher_type": entry_type,
|
"voucher_type": entry_type,
|
||||||
@ -176,6 +242,9 @@ def create_journal_entry_bts(
|
|||||||
"cheque_no": reference_number,
|
"cheque_no": reference_number,
|
||||||
"mode_of_payment": mode_of_payment,
|
"mode_of_payment": mode_of_payment,
|
||||||
}
|
}
|
||||||
|
if is_multi_currency:
|
||||||
|
journal_entry_dict.update({"multi_currency": True})
|
||||||
|
|
||||||
journal_entry = frappe.new_doc("Journal Entry")
|
journal_entry = frappe.new_doc("Journal Entry")
|
||||||
journal_entry.update(journal_entry_dict)
|
journal_entry.update(journal_entry_dict)
|
||||||
journal_entry.set("accounts", accounts)
|
journal_entry.set("accounts", accounts)
|
||||||
|
@ -2,6 +2,16 @@
|
|||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.ui.form.on("Bank Statement Import", {
|
frappe.ui.form.on("Bank Statement Import", {
|
||||||
|
onload(frm) {
|
||||||
|
frm.set_query("bank_account", function (doc) {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
company: doc.company,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
setup(frm) {
|
setup(frm) {
|
||||||
frappe.realtime.on("data_import_refresh", ({ data_import }) => {
|
frappe.realtime.on("data_import_refresh", ({ data_import }) => {
|
||||||
frm.import_in_progress = false;
|
frm.import_in_progress = false;
|
||||||
|
@ -89,7 +89,6 @@ class BankTransaction(StatusUpdater):
|
|||||||
- 0 > a: Error: already over-allocated
|
- 0 > a: Error: already over-allocated
|
||||||
- clear means: set the latest transaction date as clearance date
|
- clear means: set the latest transaction date as clearance date
|
||||||
"""
|
"""
|
||||||
gl_bank_account = frappe.db.get_value("Bank Account", self.bank_account, "account")
|
|
||||||
remaining_amount = self.unallocated_amount
|
remaining_amount = self.unallocated_amount
|
||||||
for payment_entry in self.payment_entries:
|
for payment_entry in self.payment_entries:
|
||||||
if payment_entry.allocated_amount == 0.0:
|
if payment_entry.allocated_amount == 0.0:
|
||||||
|
@ -30,7 +30,8 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "posting_date",
|
"fieldname": "posting_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"label": "Posting Date"
|
"label": "Posting Date",
|
||||||
|
"search_index": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "account_type",
|
"fieldname": "account_type",
|
||||||
@ -153,7 +154,7 @@
|
|||||||
"in_create": 1,
|
"in_create": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-06-29 12:24:20.500632",
|
"modified": "2023-10-30 16:15:00.470283",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Ledger Entry",
|
"name": "Payment Ledger Entry",
|
||||||
|
@ -229,6 +229,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
|||||||
this.data = [];
|
this.data = [];
|
||||||
const dialog = new frappe.ui.Dialog({
|
const dialog = new frappe.ui.Dialog({
|
||||||
title: __("Select Difference Account"),
|
title: __("Select Difference Account"),
|
||||||
|
size: 'extra-large',
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
fieldname: "allocation",
|
fieldname: "allocation",
|
||||||
@ -252,6 +253,13 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
|||||||
in_list_view: 1,
|
in_list_view: 1,
|
||||||
read_only: 1
|
read_only: 1
|
||||||
}, {
|
}, {
|
||||||
|
fieldtype:'Date',
|
||||||
|
fieldname:"gain_loss_posting_date",
|
||||||
|
label: __("Posting Date"),
|
||||||
|
in_list_view: 1,
|
||||||
|
reqd: 1,
|
||||||
|
}, {
|
||||||
|
|
||||||
fieldtype:'Link',
|
fieldtype:'Link',
|
||||||
options: 'Account',
|
options: 'Account',
|
||||||
in_list_view: 1,
|
in_list_view: 1,
|
||||||
@ -285,6 +293,9 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
|||||||
args.forEach(d => {
|
args.forEach(d => {
|
||||||
frappe.model.set_value("Payment Reconciliation Allocation", d.docname,
|
frappe.model.set_value("Payment Reconciliation Allocation", d.docname,
|
||||||
"difference_account", d.difference_account);
|
"difference_account", d.difference_account);
|
||||||
|
frappe.model.set_value("Payment Reconciliation Allocation", d.docname,
|
||||||
|
"gain_loss_posting_date", d.gain_loss_posting_date);
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
this.reconcile_payment_entries();
|
this.reconcile_payment_entries();
|
||||||
@ -300,6 +311,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
|||||||
'reference_name': d.reference_name,
|
'reference_name': d.reference_name,
|
||||||
'difference_amount': d.difference_amount,
|
'difference_amount': d.difference_amount,
|
||||||
'difference_account': d.difference_account,
|
'difference_account': d.difference_account,
|
||||||
|
'gain_loss_posting_date': d.gain_loss_posting_date
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -328,6 +328,7 @@ class PaymentReconciliation(Document):
|
|||||||
res.difference_amount = self.get_difference_amount(pay, inv, res["allocated_amount"])
|
res.difference_amount = self.get_difference_amount(pay, inv, res["allocated_amount"])
|
||||||
res.difference_account = default_exchange_gain_loss_account
|
res.difference_account = default_exchange_gain_loss_account
|
||||||
res.exchange_rate = inv.get("exchange_rate")
|
res.exchange_rate = inv.get("exchange_rate")
|
||||||
|
res.update({"gain_loss_posting_date": pay.get("posting_date")})
|
||||||
|
|
||||||
if pay.get("amount") == 0:
|
if pay.get("amount") == 0:
|
||||||
entries.append(res)
|
entries.append(res)
|
||||||
@ -434,6 +435,7 @@ class PaymentReconciliation(Document):
|
|||||||
"allocated_amount": flt(row.get("allocated_amount")),
|
"allocated_amount": flt(row.get("allocated_amount")),
|
||||||
"difference_amount": flt(row.get("difference_amount")),
|
"difference_amount": flt(row.get("difference_amount")),
|
||||||
"difference_account": row.get("difference_account"),
|
"difference_account": row.get("difference_account"),
|
||||||
|
"difference_posting_date": row.get("gain_loss_posting_date"),
|
||||||
"cost_center": row.get("cost_center"),
|
"cost_center": row.get("cost_center"),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -14,6 +14,7 @@ from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_pay
|
|||||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||||
from erpnext.accounts.party import get_party_account
|
from erpnext.accounts.party import get_party_account
|
||||||
|
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||||
from erpnext.stock.doctype.item.test_item import create_item
|
from erpnext.stock.doctype.item.test_item import create_item
|
||||||
|
|
||||||
test_dependencies = ["Item"]
|
test_dependencies = ["Item"]
|
||||||
@ -85,26 +86,44 @@ class TestPaymentReconciliation(FrappeTestCase):
|
|||||||
self.customer5 = make_customer("_Test PR Customer 5", "EUR")
|
self.customer5 = make_customer("_Test PR Customer 5", "EUR")
|
||||||
|
|
||||||
def create_account(self):
|
def create_account(self):
|
||||||
account_name = "Debtors EUR"
|
accounts = [
|
||||||
if not frappe.db.get_value(
|
{
|
||||||
"Account", filters={"account_name": account_name, "company": self.company}
|
"attribute": "debtors_eur",
|
||||||
):
|
"account_name": "Debtors EUR",
|
||||||
acc = frappe.new_doc("Account")
|
"parent_account": "Accounts Receivable - _PR",
|
||||||
acc.account_name = account_name
|
"account_currency": "EUR",
|
||||||
acc.parent_account = "Accounts Receivable - _PR"
|
"account_type": "Receivable",
|
||||||
acc.company = self.company
|
},
|
||||||
acc.account_currency = "EUR"
|
{
|
||||||
acc.account_type = "Receivable"
|
"attribute": "creditors_usd",
|
||||||
acc.insert()
|
"account_name": "Payable USD",
|
||||||
else:
|
"parent_account": "Accounts Payable - _PR",
|
||||||
name = frappe.db.get_value(
|
"account_currency": "USD",
|
||||||
"Account",
|
"account_type": "Payable",
|
||||||
filters={"account_name": account_name, "company": self.company},
|
},
|
||||||
fieldname="name",
|
]
|
||||||
pluck=True,
|
|
||||||
)
|
for x in accounts:
|
||||||
acc = frappe.get_doc("Account", name)
|
x = frappe._dict(x)
|
||||||
self.debtors_eur = acc.name
|
if not frappe.db.get_value(
|
||||||
|
"Account", filters={"account_name": x.account_name, "company": self.company}
|
||||||
|
):
|
||||||
|
acc = frappe.new_doc("Account")
|
||||||
|
acc.account_name = x.account_name
|
||||||
|
acc.parent_account = x.parent_account
|
||||||
|
acc.company = self.company
|
||||||
|
acc.account_currency = x.account_currency
|
||||||
|
acc.account_type = x.account_type
|
||||||
|
acc.insert()
|
||||||
|
else:
|
||||||
|
name = frappe.db.get_value(
|
||||||
|
"Account",
|
||||||
|
filters={"account_name": x.account_name, "company": self.company},
|
||||||
|
fieldname="name",
|
||||||
|
pluck=True,
|
||||||
|
)
|
||||||
|
acc = frappe.get_doc("Account", name)
|
||||||
|
setattr(self, x.attribute, acc.name)
|
||||||
|
|
||||||
def create_sales_invoice(
|
def create_sales_invoice(
|
||||||
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
|
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
|
||||||
@ -151,6 +170,64 @@ class TestPaymentReconciliation(FrappeTestCase):
|
|||||||
payment.posting_date = posting_date
|
payment.posting_date = posting_date
|
||||||
return payment
|
return payment
|
||||||
|
|
||||||
|
def create_purchase_invoice(
|
||||||
|
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Helper function to populate default values in sales invoice
|
||||||
|
"""
|
||||||
|
pinv = make_purchase_invoice(
|
||||||
|
qty=qty,
|
||||||
|
rate=rate,
|
||||||
|
company=self.company,
|
||||||
|
customer=self.supplier,
|
||||||
|
item_code=self.item,
|
||||||
|
item_name=self.item,
|
||||||
|
cost_center=self.cost_center,
|
||||||
|
warehouse=self.warehouse,
|
||||||
|
debit_to=self.debit_to,
|
||||||
|
parent_cost_center=self.cost_center,
|
||||||
|
update_stock=0,
|
||||||
|
currency="INR",
|
||||||
|
is_pos=0,
|
||||||
|
is_return=0,
|
||||||
|
return_against=None,
|
||||||
|
income_account=self.income_account,
|
||||||
|
expense_account=self.expense_account,
|
||||||
|
do_not_save=do_not_save,
|
||||||
|
do_not_submit=do_not_submit,
|
||||||
|
)
|
||||||
|
return pinv
|
||||||
|
|
||||||
|
def create_purchase_order(
|
||||||
|
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Helper function to populate default values in sales invoice
|
||||||
|
"""
|
||||||
|
pord = create_purchase_order(
|
||||||
|
qty=qty,
|
||||||
|
rate=rate,
|
||||||
|
company=self.company,
|
||||||
|
customer=self.supplier,
|
||||||
|
item_code=self.item,
|
||||||
|
item_name=self.item,
|
||||||
|
cost_center=self.cost_center,
|
||||||
|
warehouse=self.warehouse,
|
||||||
|
debit_to=self.debit_to,
|
||||||
|
parent_cost_center=self.cost_center,
|
||||||
|
update_stock=0,
|
||||||
|
currency="INR",
|
||||||
|
is_pos=0,
|
||||||
|
is_return=0,
|
||||||
|
return_against=None,
|
||||||
|
income_account=self.income_account,
|
||||||
|
expense_account=self.expense_account,
|
||||||
|
do_not_save=do_not_save,
|
||||||
|
do_not_submit=do_not_submit,
|
||||||
|
)
|
||||||
|
return pord
|
||||||
|
|
||||||
def clear_old_entries(self):
|
def clear_old_entries(self):
|
||||||
doctype_list = [
|
doctype_list = [
|
||||||
"GL Entry",
|
"GL Entry",
|
||||||
@ -163,13 +240,11 @@ class TestPaymentReconciliation(FrappeTestCase):
|
|||||||
for doctype in doctype_list:
|
for doctype in doctype_list:
|
||||||
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
|
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
|
||||||
|
|
||||||
def create_payment_reconciliation(self):
|
def create_payment_reconciliation(self, party_is_customer=True):
|
||||||
pr = frappe.new_doc("Payment Reconciliation")
|
pr = frappe.new_doc("Payment Reconciliation")
|
||||||
pr.company = self.company
|
pr.company = self.company
|
||||||
pr.party_type = (
|
pr.party_type = "Customer" if party_is_customer else "Supplier"
|
||||||
self.party_type if hasattr(self, "party_type") and self.party_type else "Customer"
|
pr.party = self.customer if party_is_customer else self.supplier
|
||||||
)
|
|
||||||
pr.party = self.customer
|
|
||||||
pr.receivable_payable_account = get_party_account(pr.party_type, pr.party, pr.company)
|
pr.receivable_payable_account = get_party_account(pr.party_type, pr.party, pr.company)
|
||||||
pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate()
|
pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate()
|
||||||
return pr
|
return pr
|
||||||
@ -906,9 +981,13 @@ class TestPaymentReconciliation(FrappeTestCase):
|
|||||||
self.assertEqual(pr.allocation[0].difference_amount, 0)
|
self.assertEqual(pr.allocation[0].difference_amount, 0)
|
||||||
|
|
||||||
def test_reconciliation_purchase_invoice_against_return(self):
|
def test_reconciliation_purchase_invoice_against_return(self):
|
||||||
pi = make_purchase_invoice(
|
self.supplier = "_Test Supplier USD"
|
||||||
supplier="_Test Supplier USD", currency="USD", conversion_rate=50
|
pi = self.create_purchase_invoice(qty=5, rate=50, do_not_submit=True)
|
||||||
).submit()
|
pi.supplier = self.supplier
|
||||||
|
pi.currency = "USD"
|
||||||
|
pi.conversion_rate = 50
|
||||||
|
pi.credit_to = self.creditors_usd
|
||||||
|
pi.save().submit()
|
||||||
|
|
||||||
pi_return = frappe.get_doc(pi.as_dict())
|
pi_return = frappe.get_doc(pi.as_dict())
|
||||||
pi_return.name = None
|
pi_return.name = None
|
||||||
@ -918,11 +997,12 @@ class TestPaymentReconciliation(FrappeTestCase):
|
|||||||
pi_return.items[0].qty = -pi_return.items[0].qty
|
pi_return.items[0].qty = -pi_return.items[0].qty
|
||||||
pi_return.submit()
|
pi_return.submit()
|
||||||
|
|
||||||
self.company = "_Test Company"
|
pr = frappe.get_doc("Payment Reconciliation")
|
||||||
self.party_type = "Supplier"
|
pr.company = self.company
|
||||||
self.customer = "_Test Supplier USD"
|
pr.party_type = "Supplier"
|
||||||
|
pr.party = self.supplier
|
||||||
pr = self.create_payment_reconciliation()
|
pr.receivable_payable_account = self.creditors_usd
|
||||||
|
pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate()
|
||||||
pr.get_unreconciled_entries()
|
pr.get_unreconciled_entries()
|
||||||
|
|
||||||
invoices = []
|
invoices = []
|
||||||
@ -931,6 +1011,7 @@ class TestPaymentReconciliation(FrappeTestCase):
|
|||||||
if invoice.invoice_number == pi.name:
|
if invoice.invoice_number == pi.name:
|
||||||
invoices.append(invoice.as_dict())
|
invoices.append(invoice.as_dict())
|
||||||
break
|
break
|
||||||
|
|
||||||
for payment in pr.payments:
|
for payment in pr.payments:
|
||||||
if payment.reference_name == pi_return.name:
|
if payment.reference_name == pi_return.name:
|
||||||
payments.append(payment.as_dict())
|
payments.append(payment.as_dict())
|
||||||
@ -941,6 +1022,121 @@ class TestPaymentReconciliation(FrappeTestCase):
|
|||||||
# Should not raise frappe.exceptions.ValidationError: Total Debit must be equal to Total Credit.
|
# Should not raise frappe.exceptions.ValidationError: Total Debit must be equal to Total Credit.
|
||||||
pr.reconcile()
|
pr.reconcile()
|
||||||
|
|
||||||
|
def test_reconciliation_from_purchase_order_to_multiple_invoices(self):
|
||||||
|
"""
|
||||||
|
Reconciling advance payment from PO/SO to multiple invoices should not cause overallocation
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.supplier = "_Test Supplier"
|
||||||
|
|
||||||
|
pi1 = self.create_purchase_invoice(qty=10, rate=100)
|
||||||
|
pi2 = self.create_purchase_invoice(qty=10, rate=100)
|
||||||
|
po = self.create_purchase_order(qty=20, rate=100)
|
||||||
|
pay = get_payment_entry(po.doctype, po.name)
|
||||||
|
# Overpay Puchase Order
|
||||||
|
pay.paid_amount = 3000
|
||||||
|
pay.save().submit()
|
||||||
|
# assert total allocated and unallocated before reconciliation
|
||||||
|
self.assertEqual(
|
||||||
|
(
|
||||||
|
pay.references[0].reference_doctype,
|
||||||
|
pay.references[0].reference_name,
|
||||||
|
pay.references[0].allocated_amount,
|
||||||
|
),
|
||||||
|
(po.doctype, po.name, 2000),
|
||||||
|
)
|
||||||
|
self.assertEqual(pay.total_allocated_amount, 2000)
|
||||||
|
self.assertEqual(pay.unallocated_amount, 1000)
|
||||||
|
self.assertEqual(pay.difference_amount, 0)
|
||||||
|
|
||||||
|
pr = self.create_payment_reconciliation(party_is_customer=False)
|
||||||
|
pr.get_unreconciled_entries()
|
||||||
|
|
||||||
|
self.assertEqual(len(pr.invoices), 2)
|
||||||
|
self.assertEqual(len(pr.payments), 2)
|
||||||
|
|
||||||
|
for x in pr.payments:
|
||||||
|
self.assertEqual((x.reference_type, x.reference_name), (pay.doctype, pay.name))
|
||||||
|
|
||||||
|
invoices = [x.as_dict() for x in pr.invoices]
|
||||||
|
payments = [x.as_dict() for x in pr.payments]
|
||||||
|
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||||
|
# partial allocation on pi1 and full allocate on pi2
|
||||||
|
pr.allocation[0].allocated_amount = 100
|
||||||
|
pr.reconcile()
|
||||||
|
|
||||||
|
# assert references and total allocated and unallocated amount
|
||||||
|
pay.reload()
|
||||||
|
self.assertEqual(len(pay.references), 3)
|
||||||
|
self.assertEqual(
|
||||||
|
(
|
||||||
|
pay.references[0].reference_doctype,
|
||||||
|
pay.references[0].reference_name,
|
||||||
|
pay.references[0].allocated_amount,
|
||||||
|
),
|
||||||
|
(po.doctype, po.name, 900),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
(
|
||||||
|
pay.references[1].reference_doctype,
|
||||||
|
pay.references[1].reference_name,
|
||||||
|
pay.references[1].allocated_amount,
|
||||||
|
),
|
||||||
|
(pi1.doctype, pi1.name, 100),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
(
|
||||||
|
pay.references[2].reference_doctype,
|
||||||
|
pay.references[2].reference_name,
|
||||||
|
pay.references[2].allocated_amount,
|
||||||
|
),
|
||||||
|
(pi2.doctype, pi2.name, 1000),
|
||||||
|
)
|
||||||
|
self.assertEqual(pay.total_allocated_amount, 2000)
|
||||||
|
self.assertEqual(pay.unallocated_amount, 1000)
|
||||||
|
self.assertEqual(pay.difference_amount, 0)
|
||||||
|
|
||||||
|
pr.get_unreconciled_entries()
|
||||||
|
self.assertEqual(len(pr.invoices), 1)
|
||||||
|
self.assertEqual(len(pr.payments), 2)
|
||||||
|
|
||||||
|
invoices = [x.as_dict() for x in pr.invoices]
|
||||||
|
payments = [x.as_dict() for x in pr.payments]
|
||||||
|
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||||
|
pr.reconcile()
|
||||||
|
|
||||||
|
# assert references and total allocated and unallocated amount
|
||||||
|
pay.reload()
|
||||||
|
self.assertEqual(len(pay.references), 3)
|
||||||
|
# PO references should be removed now
|
||||||
|
self.assertEqual(
|
||||||
|
(
|
||||||
|
pay.references[0].reference_doctype,
|
||||||
|
pay.references[0].reference_name,
|
||||||
|
pay.references[0].allocated_amount,
|
||||||
|
),
|
||||||
|
(pi1.doctype, pi1.name, 100),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
(
|
||||||
|
pay.references[1].reference_doctype,
|
||||||
|
pay.references[1].reference_name,
|
||||||
|
pay.references[1].allocated_amount,
|
||||||
|
),
|
||||||
|
(pi2.doctype, pi2.name, 1000),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
(
|
||||||
|
pay.references[2].reference_doctype,
|
||||||
|
pay.references[2].reference_name,
|
||||||
|
pay.references[2].allocated_amount,
|
||||||
|
),
|
||||||
|
(pi1.doctype, pi1.name, 900),
|
||||||
|
)
|
||||||
|
self.assertEqual(pay.total_allocated_amount, 2000)
|
||||||
|
self.assertEqual(pay.unallocated_amount, 1000)
|
||||||
|
self.assertEqual(pay.difference_amount, 0)
|
||||||
|
|
||||||
|
|
||||||
def make_customer(customer_name, currency=None):
|
def make_customer(customer_name, currency=None):
|
||||||
if not frappe.db.exists("Customer", customer_name):
|
if not frappe.db.exists("Customer", customer_name):
|
||||||
|
@ -19,6 +19,7 @@
|
|||||||
"is_advance",
|
"is_advance",
|
||||||
"section_break_5",
|
"section_break_5",
|
||||||
"difference_amount",
|
"difference_amount",
|
||||||
|
"gain_loss_posting_date",
|
||||||
"column_break_7",
|
"column_break_7",
|
||||||
"difference_account",
|
"difference_account",
|
||||||
"exchange_rate",
|
"exchange_rate",
|
||||||
@ -151,11 +152,16 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Cost Center",
|
"label": "Cost Center",
|
||||||
"options": "Cost Center"
|
"options": "Cost Center"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "gain_loss_posting_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "Difference Posting Date"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-09-03 07:52:33.684217",
|
"modified": "2023-10-23 10:44:56.066303",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Payment Reconciliation Allocation",
|
"name": "Payment Reconciliation Allocation",
|
||||||
|
@ -1,13 +1,9 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
|
|
||||||
# For license information, please see license.txt
|
|
||||||
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import flt, get_url, nowdate
|
from frappe.utils import flt, nowdate
|
||||||
from frappe.utils.background_jobs import enqueue
|
from frappe.utils.background_jobs import enqueue
|
||||||
|
|
||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||||
@ -20,7 +16,6 @@ from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
|||||||
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
|
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
|
||||||
from erpnext.accounts.party import get_party_account, get_party_bank_account
|
from erpnext.accounts.party import get_party_account, get_party_bank_account
|
||||||
from erpnext.accounts.utils import get_account_currency
|
from erpnext.accounts.utils import get_account_currency
|
||||||
from erpnext.erpnext_integrations.stripe_integration import create_stripe_subscription
|
|
||||||
from erpnext.utilities import payment_app_import_guard
|
from erpnext.utilities import payment_app_import_guard
|
||||||
|
|
||||||
|
|
||||||
@ -412,35 +407,11 @@ class PaymentRequest(Document):
|
|||||||
def get_payment_success_url(self):
|
def get_payment_success_url(self):
|
||||||
return self.payment_success_url
|
return self.payment_success_url
|
||||||
|
|
||||||
def on_payment_authorized(self, status=None):
|
|
||||||
if not status:
|
|
||||||
return
|
|
||||||
|
|
||||||
shopping_cart_settings = frappe.get_doc("E Commerce Settings")
|
|
||||||
|
|
||||||
if status in ["Authorized", "Completed"]:
|
|
||||||
redirect_to = None
|
|
||||||
self.set_as_paid()
|
|
||||||
|
|
||||||
# if shopping cart enabled and in session
|
|
||||||
if (
|
|
||||||
shopping_cart_settings.enabled
|
|
||||||
and hasattr(frappe.local, "session")
|
|
||||||
and frappe.local.session.user != "Guest"
|
|
||||||
) and self.payment_channel != "Phone":
|
|
||||||
|
|
||||||
success_url = shopping_cart_settings.payment_success_url
|
|
||||||
if success_url:
|
|
||||||
redirect_to = ({"Orders": "/orders", "Invoices": "/invoices", "My Account": "/me"}).get(
|
|
||||||
success_url, "/me"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
redirect_to = get_url("/orders/{0}".format(self.reference_name))
|
|
||||||
|
|
||||||
return redirect_to
|
|
||||||
|
|
||||||
def create_subscription(self, payment_provider, gateway_controller, data):
|
def create_subscription(self, payment_provider, gateway_controller, data):
|
||||||
if payment_provider == "stripe":
|
if payment_provider == "stripe":
|
||||||
|
with payment_app_import_guard():
|
||||||
|
from payments.payment_gateways.stripe_integration import create_stripe_subscription
|
||||||
|
|
||||||
return create_stripe_subscription(gateway_controller, data)
|
return create_stripe_subscription(gateway_controller, data)
|
||||||
|
|
||||||
|
|
||||||
@ -592,13 +563,12 @@ def get_existing_payment_request_amount(ref_dt, ref_dn):
|
|||||||
|
|
||||||
|
|
||||||
def get_gateway_details(args): # nosemgrep
|
def get_gateway_details(args): # nosemgrep
|
||||||
"""return gateway and payment account of default payment gateway"""
|
"""
|
||||||
if args.get("payment_gateway_account"):
|
Return gateway and payment account of default payment gateway
|
||||||
return get_payment_gateway_account(args.get("payment_gateway_account"))
|
"""
|
||||||
|
gateway_account = args.get("payment_gateway_account", {"is_default": 1})
|
||||||
if args.order_type == "Shopping Cart":
|
if gateway_account:
|
||||||
payment_gateway_account = frappe.get_doc("E Commerce Settings").payment_gateway_account
|
return get_payment_gateway_account(gateway_account)
|
||||||
return get_payment_gateway_account(payment_gateway_account)
|
|
||||||
|
|
||||||
gateway_account = get_payment_gateway_account({"is_default": 1})
|
gateway_account = get_payment_gateway_account({"is_default": 1})
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import unittest
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from frappe.utils import add_days, nowdate
|
||||||
|
|
||||||
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
|
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
|
||||||
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
|
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
|
||||||
@ -125,70 +126,64 @@ class TestPOSInvoice(unittest.TestCase):
|
|||||||
self.assertEqual(inv.grand_total, 5474.0)
|
self.assertEqual(inv.grand_total, 5474.0)
|
||||||
|
|
||||||
def test_tax_calculation_with_item_tax_template(self):
|
def test_tax_calculation_with_item_tax_template(self):
|
||||||
inv = create_pos_invoice(qty=84, rate=4.6, do_not_save=1)
|
import json
|
||||||
item_row = inv.get("items")[0]
|
|
||||||
|
|
||||||
add_items = [
|
from erpnext.stock.get_item_details import get_item_details
|
||||||
(54, "_Test Account Excise Duty @ 12 - _TC"),
|
|
||||||
(288, "_Test Account Excise Duty @ 15 - _TC"),
|
# set tax template in item
|
||||||
(144, "_Test Account Excise Duty @ 20 - _TC"),
|
item = frappe.get_cached_doc("Item", "_Test Item")
|
||||||
(430, "_Test Item Tax Template 1 - _TC"),
|
item.set(
|
||||||
|
"taxes",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"item_tax_template": "_Test Account Excise Duty @ 15 - _TC",
|
||||||
|
"valid_from": add_days(nowdate(), -5),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
# create POS invoice with item
|
||||||
|
pos_inv = create_pos_invoice(qty=84, rate=4.6, do_not_save=True)
|
||||||
|
item_details = get_item_details(
|
||||||
|
doc=pos_inv,
|
||||||
|
args={
|
||||||
|
"item_code": item.item_code,
|
||||||
|
"company": pos_inv.company,
|
||||||
|
"doctype": "POS Invoice",
|
||||||
|
"conversion_rate": 1.0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
tax_map = json.loads(item_details.item_tax_rate)
|
||||||
|
for tax in tax_map:
|
||||||
|
pos_inv.append(
|
||||||
|
"taxes",
|
||||||
|
{
|
||||||
|
"charge_type": "On Net Total",
|
||||||
|
"account_head": tax,
|
||||||
|
"rate": tax_map[tax],
|
||||||
|
"description": "Test",
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
pos_inv.submit()
|
||||||
|
pos_inv.load_from_db()
|
||||||
|
|
||||||
|
# check if correct tax values are applied from tax template
|
||||||
|
self.assertEqual(pos_inv.net_total, 386.4)
|
||||||
|
|
||||||
|
expected_taxes = [
|
||||||
|
{
|
||||||
|
"tax_amount": 57.96,
|
||||||
|
"total": 444.36,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
for qty, item_tax_template in add_items:
|
|
||||||
item_row_copy = copy.deepcopy(item_row)
|
|
||||||
item_row_copy.qty = qty
|
|
||||||
item_row_copy.item_tax_template = item_tax_template
|
|
||||||
inv.append("items", item_row_copy)
|
|
||||||
|
|
||||||
inv.append(
|
for i in range(len(expected_taxes)):
|
||||||
"taxes",
|
for key in expected_taxes[i]:
|
||||||
{
|
self.assertEqual(expected_taxes[i][key], pos_inv.get("taxes")[i].get(key))
|
||||||
"account_head": "_Test Account Excise Duty - _TC",
|
|
||||||
"charge_type": "On Net Total",
|
|
||||||
"cost_center": "_Test Cost Center - _TC",
|
|
||||||
"description": "Excise Duty",
|
|
||||||
"doctype": "Sales Taxes and Charges",
|
|
||||||
"rate": 11,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
inv.append(
|
|
||||||
"taxes",
|
|
||||||
{
|
|
||||||
"account_head": "_Test Account Education Cess - _TC",
|
|
||||||
"charge_type": "On Net Total",
|
|
||||||
"cost_center": "_Test Cost Center - _TC",
|
|
||||||
"description": "Education Cess",
|
|
||||||
"doctype": "Sales Taxes and Charges",
|
|
||||||
"rate": 0,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
inv.append(
|
|
||||||
"taxes",
|
|
||||||
{
|
|
||||||
"account_head": "_Test Account S&H Education Cess - _TC",
|
|
||||||
"charge_type": "On Net Total",
|
|
||||||
"cost_center": "_Test Cost Center - _TC",
|
|
||||||
"description": "S&H Education Cess",
|
|
||||||
"doctype": "Sales Taxes and Charges",
|
|
||||||
"rate": 3,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
inv.insert()
|
|
||||||
|
|
||||||
self.assertEqual(inv.net_total, 4600)
|
self.assertEqual(pos_inv.get("base_total_taxes_and_charges"), 57.96)
|
||||||
|
|
||||||
self.assertEqual(inv.get("taxes")[0].tax_amount, 502.41)
|
|
||||||
self.assertEqual(inv.get("taxes")[0].total, 5102.41)
|
|
||||||
|
|
||||||
self.assertEqual(inv.get("taxes")[1].tax_amount, 197.80)
|
|
||||||
self.assertEqual(inv.get("taxes")[1].total, 5300.21)
|
|
||||||
|
|
||||||
self.assertEqual(inv.get("taxes")[2].tax_amount, 375.36)
|
|
||||||
self.assertEqual(inv.get("taxes")[2].total, 5675.57)
|
|
||||||
|
|
||||||
self.assertEqual(inv.grand_total, 5675.57)
|
|
||||||
self.assertEqual(inv.rounding_adjustment, 0.43)
|
|
||||||
self.assertEqual(inv.rounded_total, 5676.0)
|
|
||||||
|
|
||||||
def test_tax_calculation_with_multiple_items_and_discount(self):
|
def test_tax_calculation_with_multiple_items_and_discount(self):
|
||||||
inv = create_pos_invoice(qty=1, rate=75, do_not_save=True)
|
inv = create_pos_invoice(qty=1, rate=75, do_not_save=True)
|
||||||
@ -776,19 +771,28 @@ class TestPOSInvoice(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
create_batch_item_with_batch("_BATCH ITEM Test For Reserve", "TestBatch-RS 02")
|
create_batch_item_with_batch("_BATCH ITEM Test For Reserve", "TestBatch-RS 02")
|
||||||
make_stock_entry(
|
se = make_stock_entry(
|
||||||
target="_Test Warehouse - _TC",
|
target="_Test Warehouse - _TC",
|
||||||
item_code="_BATCH ITEM Test For Reserve",
|
item_code="_BATCH ITEM Test For Reserve",
|
||||||
qty=20,
|
qty=30,
|
||||||
basic_rate=100,
|
basic_rate=100,
|
||||||
batch_no="TestBatch-RS 02",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
se.reload()
|
||||||
|
|
||||||
|
batch_no = get_batch_from_bundle(se.items[0].serial_and_batch_bundle)
|
||||||
|
|
||||||
|
# POS Invoice 1, for the batch without bundle
|
||||||
pos_inv1 = create_pos_invoice(
|
pos_inv1 = create_pos_invoice(
|
||||||
item="_BATCH ITEM Test For Reserve", rate=300, qty=15, batch_no="TestBatch-RS 02"
|
item="_BATCH ITEM Test For Reserve", rate=300, qty=15, do_not_save=1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
pos_inv1.items[0].batch_no = batch_no
|
||||||
pos_inv1.save()
|
pos_inv1.save()
|
||||||
pos_inv1.submit()
|
pos_inv1.submit()
|
||||||
|
pos_inv1.reload()
|
||||||
|
|
||||||
|
self.assertFalse(pos_inv1.items[0].serial_and_batch_bundle)
|
||||||
|
|
||||||
batches = get_auto_batch_nos(
|
batches = get_auto_batch_nos(
|
||||||
frappe._dict(
|
frappe._dict(
|
||||||
@ -797,7 +801,24 @@ class TestPOSInvoice(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
for batch in batches:
|
for batch in batches:
|
||||||
if batch.batch_no == "TestBatch-RS 02" and batch.warehouse == "_Test Warehouse - _TC":
|
if batch.batch_no == batch_no and batch.warehouse == "_Test Warehouse - _TC":
|
||||||
|
self.assertEqual(batch.qty, 15)
|
||||||
|
|
||||||
|
# POS Invoice 2, for the batch with bundle
|
||||||
|
pos_inv2 = create_pos_invoice(
|
||||||
|
item="_BATCH ITEM Test For Reserve", rate=300, qty=10, batch_no=batch_no
|
||||||
|
)
|
||||||
|
pos_inv2.reload()
|
||||||
|
self.assertTrue(pos_inv2.items[0].serial_and_batch_bundle)
|
||||||
|
|
||||||
|
batches = get_auto_batch_nos(
|
||||||
|
frappe._dict(
|
||||||
|
{"item_code": "_BATCH ITEM Test For Reserve", "warehouse": "_Test Warehouse - _TC"}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for batch in batches:
|
||||||
|
if batch.batch_no == batch_no and batch.warehouse == "_Test Warehouse - _TC":
|
||||||
self.assertEqual(batch.qty, 5)
|
self.assertEqual(batch.qty, 5)
|
||||||
|
|
||||||
def test_pos_batch_item_qty_validation(self):
|
def test_pos_batch_item_qty_validation(self):
|
||||||
|
@ -454,7 +454,7 @@ def create_merge_logs(invoice_by_customer, closing_entry=None):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
message_log = frappe.message_log.pop() if frappe.message_log else str(e)
|
message_log = frappe.message_log.pop() if frappe.message_log else str(e)
|
||||||
error_message = safe_load_json(message_log)
|
error_message = get_error_message(message_log)
|
||||||
|
|
||||||
if closing_entry:
|
if closing_entry:
|
||||||
closing_entry.set_status(update=True, status="Failed")
|
closing_entry.set_status(update=True, status="Failed")
|
||||||
@ -483,7 +483,7 @@ def cancel_merge_logs(merge_logs, closing_entry=None):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
message_log = frappe.message_log.pop() if frappe.message_log else str(e)
|
message_log = frappe.message_log.pop() if frappe.message_log else str(e)
|
||||||
error_message = safe_load_json(message_log)
|
error_message = get_error_message(message_log)
|
||||||
|
|
||||||
if closing_entry:
|
if closing_entry:
|
||||||
closing_entry.set_status(update=True, status="Submitted")
|
closing_entry.set_status(update=True, status="Submitted")
|
||||||
@ -525,10 +525,8 @@ def check_scheduler_status():
|
|||||||
frappe.throw(_("Scheduler is inactive. Cannot enqueue job."), title=_("Scheduler Inactive"))
|
frappe.throw(_("Scheduler is inactive. Cannot enqueue job."), title=_("Scheduler Inactive"))
|
||||||
|
|
||||||
|
|
||||||
def safe_load_json(message):
|
def get_error_message(message) -> str:
|
||||||
try:
|
try:
|
||||||
json_message = json.loads(message).get("message")
|
return message["message"]
|
||||||
except Exception:
|
except Exception:
|
||||||
json_message = message
|
return str(message)
|
||||||
|
|
||||||
return json_message
|
|
||||||
|
@ -36,6 +36,7 @@
|
|||||||
"currency_and_price_list",
|
"currency_and_price_list",
|
||||||
"currency",
|
"currency",
|
||||||
"conversion_rate",
|
"conversion_rate",
|
||||||
|
"use_transaction_date_exchange_rate",
|
||||||
"column_break2",
|
"column_break2",
|
||||||
"buying_price_list",
|
"buying_price_list",
|
||||||
"price_list_currency",
|
"price_list_currency",
|
||||||
@ -1588,13 +1589,20 @@
|
|||||||
"label": "Repost Required",
|
"label": "Repost Required",
|
||||||
"options": "Account",
|
"options": "Account",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "use_transaction_date_exchange_rate",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Use Transaction Date Exchange Rate",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-file-text",
|
"icon": "fa fa-file-text",
|
||||||
"idx": 204,
|
"idx": 204,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-10-01 21:01:47.282533",
|
"modified": "2023-10-16 16:24:51.886231",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Purchase Invoice",
|
"name": "Purchase Invoice",
|
||||||
|
@ -33,7 +33,7 @@ from erpnext.accounts.general_ledger import (
|
|||||||
)
|
)
|
||||||
from erpnext.accounts.party import get_due_date, get_party_account
|
from erpnext.accounts.party import get_due_date, get_party_account
|
||||||
from erpnext.accounts.utils import get_account_currency, get_fiscal_year
|
from erpnext.accounts.utils import get_account_currency, get_fiscal_year
|
||||||
from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled
|
from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled
|
||||||
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
|
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
|
||||||
from erpnext.buying.utils import check_on_hold_or_closed_status
|
from erpnext.buying.utils import check_on_hold_or_closed_status
|
||||||
from erpnext.controllers.accounts_controller import validate_account_head
|
from erpnext.controllers.accounts_controller import validate_account_head
|
||||||
@ -281,9 +281,6 @@ class PurchaseInvoice(BuyingController):
|
|||||||
# in case of auto inventory accounting,
|
# in case of auto inventory accounting,
|
||||||
# expense account is always "Stock Received But Not Billed" for a stock item
|
# expense account is always "Stock Received But Not Billed" for a stock item
|
||||||
# except opening entry, drop-ship entry and fixed asset items
|
# except opening entry, drop-ship entry and fixed asset items
|
||||||
if item.item_code:
|
|
||||||
asset_category = frappe.get_cached_value("Item", item.item_code, "asset_category")
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
auto_accounting_for_stock
|
auto_accounting_for_stock
|
||||||
and item.item_code in stock_items
|
and item.item_code in stock_items
|
||||||
@ -350,22 +347,26 @@ class PurchaseInvoice(BuyingController):
|
|||||||
frappe.msgprint(msg, title=_("Expense Head Changed"))
|
frappe.msgprint(msg, title=_("Expense Head Changed"))
|
||||||
|
|
||||||
item.expense_account = stock_not_billed_account
|
item.expense_account = stock_not_billed_account
|
||||||
|
elif item.is_fixed_asset and item.pr_detail:
|
||||||
elif item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category):
|
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 item.is_fixed_asset:
|
||||||
|
account_type = (
|
||||||
|
"capital_work_in_progress_account"
|
||||||
|
if is_cwip_accounting_enabled(item.asset_category)
|
||||||
|
else "fixed_asset_account"
|
||||||
|
)
|
||||||
asset_category_account = get_asset_category_account(
|
asset_category_account = get_asset_category_account(
|
||||||
"fixed_asset_account", item=item.item_code, company=self.company
|
account_type, item=item.item_code, company=self.company
|
||||||
)
|
)
|
||||||
if not asset_category_account:
|
if not asset_category_account:
|
||||||
form_link = get_link_to_form("Asset Category", asset_category)
|
form_link = get_link_to_form("Asset Category", item.asset_category)
|
||||||
throw(
|
throw(
|
||||||
_("Please set Fixed Asset Account in {} against {}.").format(form_link, self.company),
|
_("Please set Fixed Asset Account in {} against {}.").format(form_link, self.company),
|
||||||
title=_("Missing Account"),
|
title=_("Missing Account"),
|
||||||
)
|
)
|
||||||
item.expense_account = asset_category_account
|
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:
|
elif not item.expense_account and for_validate:
|
||||||
throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name))
|
throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name))
|
||||||
|
|
||||||
@ -539,8 +540,9 @@ class PurchaseInvoice(BuyingController):
|
|||||||
]
|
]
|
||||||
child_tables = {"items": ("expense_account",), "taxes": ("account_head",)}
|
child_tables = {"items": ("expense_account",), "taxes": ("account_head",)}
|
||||||
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
|
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
|
||||||
self.validate_for_repost()
|
if self.needs_repost:
|
||||||
self.db_set("repost_required", self.needs_repost)
|
self.validate_for_repost()
|
||||||
|
self.db_set("repost_required", self.needs_repost)
|
||||||
|
|
||||||
def make_gl_entries(self, gl_entries=None, from_repost=False):
|
def make_gl_entries(self, gl_entries=None, from_repost=False):
|
||||||
if not gl_entries:
|
if not gl_entries:
|
||||||
@ -583,12 +585,11 @@ class PurchaseInvoice(BuyingController):
|
|||||||
|
|
||||||
def get_gl_entries(self, warehouse_account=None):
|
def get_gl_entries(self, warehouse_account=None):
|
||||||
self.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company)
|
self.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company)
|
||||||
|
|
||||||
if self.auto_accounting_for_stock:
|
if self.auto_accounting_for_stock:
|
||||||
self.stock_received_but_not_billed = self.get_company_default("stock_received_but_not_billed")
|
self.stock_received_but_not_billed = self.get_company_default("stock_received_but_not_billed")
|
||||||
self.expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
|
|
||||||
else:
|
else:
|
||||||
self.stock_received_but_not_billed = None
|
self.stock_received_but_not_billed = None
|
||||||
self.expenses_included_in_valuation = None
|
|
||||||
|
|
||||||
self.negative_expense_to_be_booked = 0.0
|
self.negative_expense_to_be_booked = 0.0
|
||||||
gl_entries = []
|
gl_entries = []
|
||||||
@ -597,9 +598,6 @@ class PurchaseInvoice(BuyingController):
|
|||||||
self.make_item_gl_entries(gl_entries)
|
self.make_item_gl_entries(gl_entries)
|
||||||
self.make_precision_loss_gl_entry(gl_entries)
|
self.make_precision_loss_gl_entry(gl_entries)
|
||||||
|
|
||||||
if self.check_asset_cwip_enabled():
|
|
||||||
self.get_asset_gl_entry(gl_entries)
|
|
||||||
|
|
||||||
self.make_tax_gl_entries(gl_entries)
|
self.make_tax_gl_entries(gl_entries)
|
||||||
self.make_internal_transfer_gl_entries(gl_entries)
|
self.make_internal_transfer_gl_entries(gl_entries)
|
||||||
|
|
||||||
@ -701,7 +699,11 @@ class PurchaseInvoice(BuyingController):
|
|||||||
if item.item_code:
|
if item.item_code:
|
||||||
asset_category = frappe.get_cached_value("Item", item.item_code, "asset_category")
|
asset_category = frappe.get_cached_value("Item", item.item_code, "asset_category")
|
||||||
|
|
||||||
if self.update_stock and self.auto_accounting_for_stock and item.item_code in stock_items:
|
if (
|
||||||
|
self.update_stock
|
||||||
|
and self.auto_accounting_for_stock
|
||||||
|
and (item.item_code in stock_items or item.is_fixed_asset)
|
||||||
|
):
|
||||||
# warehouse account
|
# warehouse account
|
||||||
warehouse_debit_amount = self.make_stock_adjustment_entry(
|
warehouse_debit_amount = self.make_stock_adjustment_entry(
|
||||||
gl_entries, item, voucher_wise_stock_value, account_currency
|
gl_entries, item, voucher_wise_stock_value, account_currency
|
||||||
@ -816,9 +818,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
elif not item.is_fixed_asset or (
|
else:
|
||||||
item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category)
|
|
||||||
):
|
|
||||||
expense_account = (
|
expense_account = (
|
||||||
item.expense_account
|
item.expense_account
|
||||||
if (not item.enable_deferred_expense or self.is_return)
|
if (not item.enable_deferred_expense or self.is_return)
|
||||||
@ -911,40 +911,6 @@ class PurchaseInvoice(BuyingController):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# If asset is bought through this document and not linked to PR
|
|
||||||
if self.update_stock and item.landed_cost_voucher_amount:
|
|
||||||
expenses_included_in_asset_valuation = self.get_company_default(
|
|
||||||
"expenses_included_in_asset_valuation"
|
|
||||||
)
|
|
||||||
# Amount added through landed-cost-voucher
|
|
||||||
gl_entries.append(
|
|
||||||
self.get_gl_dict(
|
|
||||||
{
|
|
||||||
"account": expenses_included_in_asset_valuation,
|
|
||||||
"against": expense_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,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
gl_entries.append(
|
|
||||||
self.get_gl_dict(
|
|
||||||
{
|
|
||||||
"account": expense_account,
|
|
||||||
"against": expenses_included_in_asset_valuation,
|
|
||||||
"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 asset bought through this document
|
# update gross amount of asset bought through this document
|
||||||
assets = frappe.db.get_all(
|
assets = frappe.db.get_all(
|
||||||
"Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code}
|
"Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code}
|
||||||
@ -969,11 +935,17 @@ class PurchaseInvoice(BuyingController):
|
|||||||
(item.purchase_receipt, valuation_tax_accounts),
|
(item.purchase_receipt, valuation_tax_accounts),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
stock_rbnb = (
|
||||||
|
self.get_company_default("asset_received_but_not_billed")
|
||||||
|
if item.is_fixed_asset
|
||||||
|
else self.stock_received_but_not_billed
|
||||||
|
)
|
||||||
|
|
||||||
if not negative_expense_booked_in_pr:
|
if not negative_expense_booked_in_pr:
|
||||||
gl_entries.append(
|
gl_entries.append(
|
||||||
self.get_gl_dict(
|
self.get_gl_dict(
|
||||||
{
|
{
|
||||||
"account": self.stock_received_but_not_billed,
|
"account": stock_rbnb,
|
||||||
"against": self.supplier,
|
"against": self.supplier,
|
||||||
"debit": flt(item.item_tax_amount, item.precision("item_tax_amount")),
|
"debit": flt(item.item_tax_amount, item.precision("item_tax_amount")),
|
||||||
"remarks": self.remarks or _("Accounting Entry for Stock"),
|
"remarks": self.remarks or _("Accounting Entry for Stock"),
|
||||||
@ -988,156 +960,12 @@ class PurchaseInvoice(BuyingController):
|
|||||||
item.item_tax_amount, item.precision("item_tax_amount")
|
item.item_tax_amount, item.precision("item_tax_amount")
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_asset_gl_entry(self, gl_entries):
|
assets = frappe.db.get_all(
|
||||||
arbnb_account = None
|
"Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code}
|
||||||
eiiav_account = None
|
)
|
||||||
asset_eiiav_currency = None
|
for asset in assets:
|
||||||
|
frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate))
|
||||||
for item in self.get("items"):
|
frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate))
|
||||||
if item.is_fixed_asset:
|
|
||||||
asset_amount = flt(item.net_amount) + flt(item.item_tax_amount / self.conversion_rate)
|
|
||||||
base_asset_amount = flt(item.base_net_amount + item.item_tax_amount)
|
|
||||||
|
|
||||||
item_exp_acc_type = frappe.get_cached_value("Account", item.expense_account, "account_type")
|
|
||||||
if not item.expense_account or item_exp_acc_type not in [
|
|
||||||
"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:
|
|
||||||
arbnb_currency = get_account_currency(item.expense_account)
|
|
||||||
gl_entries.append(
|
|
||||||
self.get_gl_dict(
|
|
||||||
{
|
|
||||||
"account": item.expense_account,
|
|
||||||
"against": self.supplier,
|
|
||||||
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
|
|
||||||
"debit": base_asset_amount,
|
|
||||||
"debit_in_account_currency": (
|
|
||||||
base_asset_amount if arbnb_currency == self.company_currency else asset_amount
|
|
||||||
),
|
|
||||||
"cost_center": item.cost_center,
|
|
||||||
"project": item.project or self.project,
|
|
||||||
},
|
|
||||||
item=item,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if item.item_tax_amount:
|
|
||||||
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(
|
|
||||||
{
|
|
||||||
"account": eiiav_account,
|
|
||||||
"against": self.supplier,
|
|
||||||
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
|
|
||||||
"cost_center": item.cost_center,
|
|
||||||
"project": item.project or self.project,
|
|
||||||
"credit": item.item_tax_amount,
|
|
||||||
"credit_in_account_currency": (
|
|
||||||
item.item_tax_amount
|
|
||||||
if asset_eiiav_currency == self.company_currency
|
|
||||||
else item.item_tax_amount / self.conversion_rate
|
|
||||||
),
|
|
||||||
},
|
|
||||||
item=item,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
cwip_account = get_asset_account(
|
|
||||||
"capital_work_in_progress_account", asset_category=item.asset_category, company=self.company
|
|
||||||
)
|
|
||||||
|
|
||||||
cwip_account_currency = get_account_currency(cwip_account)
|
|
||||||
gl_entries.append(
|
|
||||||
self.get_gl_dict(
|
|
||||||
{
|
|
||||||
"account": cwip_account,
|
|
||||||
"against": self.supplier,
|
|
||||||
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
|
|
||||||
"debit": base_asset_amount,
|
|
||||||
"debit_in_account_currency": (
|
|
||||||
base_asset_amount if cwip_account_currency == self.company_currency else asset_amount
|
|
||||||
),
|
|
||||||
"cost_center": self.cost_center,
|
|
||||||
"project": item.project or self.project,
|
|
||||||
},
|
|
||||||
item=item,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if item.item_tax_amount and not cint(erpnext.is_perpetual_inventory_enabled(self.company)):
|
|
||||||
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(
|
|
||||||
{
|
|
||||||
"account": eiiav_account,
|
|
||||||
"against": self.supplier,
|
|
||||||
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
|
|
||||||
"cost_center": item.cost_center,
|
|
||||||
"credit": item.item_tax_amount,
|
|
||||||
"project": item.project or self.project,
|
|
||||||
"credit_in_account_currency": (
|
|
||||||
item.item_tax_amount
|
|
||||||
if asset_eiiav_currency == self.company_currency
|
|
||||||
else item.item_tax_amount / self.conversion_rate
|
|
||||||
),
|
|
||||||
},
|
|
||||||
item=item,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Assets are bought through this document then it will be linked to this document
|
|
||||||
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": 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,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
def make_stock_adjustment_entry(
|
def make_stock_adjustment_entry(
|
||||||
self, gl_entries, item, voucher_wise_stock_value, account_currency
|
self, gl_entries, item, voucher_wise_stock_value, account_currency
|
||||||
@ -1836,6 +1664,7 @@ def make_purchase_receipt(source_name, target_doc=None):
|
|||||||
"po_detail": "purchase_order_item",
|
"po_detail": "purchase_order_item",
|
||||||
"material_request": "material_request",
|
"material_request": "material_request",
|
||||||
"material_request_item": "material_request_item",
|
"material_request_item": "material_request_item",
|
||||||
|
"wip_composite_asset": "wip_composite_asset",
|
||||||
},
|
},
|
||||||
"postprocess": update_item,
|
"postprocess": update_item,
|
||||||
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty),
|
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty),
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.tests.utils import change_settings
|
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||||
from frappe.utils import add_days, cint, flt, getdate, nowdate, today
|
from frappe.utils import add_days, cint, flt, getdate, nowdate, today
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
@ -38,7 +38,7 @@ test_dependencies = ["Item", "Cost Center", "Payment Term", "Payment Terms Templ
|
|||||||
test_ignore = ["Serial No"]
|
test_ignore = ["Serial No"]
|
||||||
|
|
||||||
|
|
||||||
class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(self):
|
def setUpClass(self):
|
||||||
unlink_payment_on_cancel_of_invoice()
|
unlink_payment_on_cancel_of_invoice()
|
||||||
@ -48,6 +48,9 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
|||||||
def tearDownClass(self):
|
def tearDownClass(self):
|
||||||
unlink_payment_on_cancel_of_invoice(0)
|
unlink_payment_on_cancel_of_invoice(0)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
frappe.db.rollback()
|
||||||
|
|
||||||
def test_purchase_invoice_received_qty(self):
|
def test_purchase_invoice_received_qty(self):
|
||||||
"""
|
"""
|
||||||
1. Test if received qty is validated against accepted + rejected
|
1. Test if received qty is validated against accepted + rejected
|
||||||
@ -422,6 +425,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
|||||||
self.assertEqual(tax.tax_amount, expected_values[i][1])
|
self.assertEqual(tax.tax_amount, expected_values[i][1])
|
||||||
self.assertEqual(tax.total, expected_values[i][2])
|
self.assertEqual(tax.total, expected_values[i][2])
|
||||||
|
|
||||||
|
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
|
||||||
def test_purchase_invoice_with_advance(self):
|
def test_purchase_invoice_with_advance(self):
|
||||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import (
|
from erpnext.accounts.doctype.journal_entry.test_journal_entry import (
|
||||||
test_records as jv_test_records,
|
test_records as jv_test_records,
|
||||||
@ -476,6 +480,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
|
||||||
def test_invoice_with_advance_and_multi_payment_terms(self):
|
def test_invoice_with_advance_and_multi_payment_terms(self):
|
||||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import (
|
from erpnext.accounts.doctype.journal_entry.test_journal_entry import (
|
||||||
test_records as jv_test_records,
|
test_records as jv_test_records,
|
||||||
@ -1220,6 +1225,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
|||||||
acc_settings.submit_journal_entriessubmit_journal_entries = 0
|
acc_settings.submit_journal_entriessubmit_journal_entries = 0
|
||||||
acc_settings.save()
|
acc_settings.save()
|
||||||
|
|
||||||
|
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
|
||||||
def test_gain_loss_with_advance_entry(self):
|
def test_gain_loss_with_advance_entry(self):
|
||||||
unlink_enabled = frappe.db.get_value(
|
unlink_enabled = frappe.db.get_value(
|
||||||
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice"
|
"Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice"
|
||||||
@ -1420,6 +1426,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
|||||||
)
|
)
|
||||||
frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", original_account)
|
frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", original_account)
|
||||||
|
|
||||||
|
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
|
||||||
def test_purchase_invoice_advance_taxes(self):
|
def test_purchase_invoice_advance_taxes(self):
|
||||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||||
|
|
||||||
@ -1942,6 +1949,30 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
|||||||
self.assertEqual(po.docstatus, 1)
|
self.assertEqual(po.docstatus, 1)
|
||||||
self.assertEqual(pi.docstatus, 1)
|
self.assertEqual(pi.docstatus, 1)
|
||||||
|
|
||||||
|
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 set_advance_flag(company, flag, default_account):
|
def set_advance_flag(company, flag, default_account):
|
||||||
frappe.db.set_value(
|
frappe.db.set_value(
|
||||||
|
@ -536,8 +536,9 @@ class SalesInvoice(SellingController):
|
|||||||
"taxes": ("account_head",),
|
"taxes": ("account_head",),
|
||||||
}
|
}
|
||||||
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
|
self.needs_repost = self.check_if_fields_updated(fields_to_check, child_tables)
|
||||||
self.validate_for_repost()
|
if self.needs_repost:
|
||||||
self.db_set("repost_required", self.needs_repost)
|
self.validate_for_repost()
|
||||||
|
self.db_set("repost_required", self.needs_repost)
|
||||||
|
|
||||||
def set_paid_amount(self):
|
def set_paid_amount(self):
|
||||||
paid_amount = 0.0
|
paid_amount = 0.0
|
||||||
|
@ -6,7 +6,7 @@ import unittest
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.model.dynamic_links import get_dynamic_link_map
|
from frappe.model.dynamic_links import get_dynamic_link_map
|
||||||
from frappe.tests.utils import change_settings
|
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||||
from frappe.utils import add_days, flt, getdate, nowdate, today
|
from frappe.utils import add_days, flt, getdate, nowdate, today
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
@ -45,13 +45,17 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
|
|||||||
from erpnext.stock.utils import get_incoming_rate, get_stock_balance
|
from erpnext.stock.utils import get_incoming_rate, get_stock_balance
|
||||||
|
|
||||||
|
|
||||||
class TestSalesInvoice(unittest.TestCase):
|
class TestSalesInvoice(FrappeTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
from erpnext.stock.doctype.stock_ledger_entry.test_stock_ledger_entry import create_items
|
from erpnext.stock.doctype.stock_ledger_entry.test_stock_ledger_entry import create_items
|
||||||
|
|
||||||
create_items(["_Test Internal Transfer Item"], uoms=[{"uom": "Box", "conversion_factor": 10}])
|
create_items(["_Test Internal Transfer Item"], uoms=[{"uom": "Box", "conversion_factor": 10}])
|
||||||
create_internal_parties()
|
create_internal_parties()
|
||||||
setup_accounts()
|
setup_accounts()
|
||||||
|
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
frappe.db.rollback()
|
||||||
|
|
||||||
def make(self):
|
def make(self):
|
||||||
w = frappe.copy_doc(test_records[0])
|
w = frappe.copy_doc(test_records[0])
|
||||||
@ -179,6 +183,7 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
self.assertRaises(frappe.LinkExistsError, si.cancel)
|
self.assertRaises(frappe.LinkExistsError, si.cancel)
|
||||||
unlink_payment_on_cancel_of_invoice()
|
unlink_payment_on_cancel_of_invoice()
|
||||||
|
|
||||||
|
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
|
||||||
def test_payment_entry_unlink_against_standalone_credit_note(self):
|
def test_payment_entry_unlink_against_standalone_credit_note(self):
|
||||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||||
|
|
||||||
@ -511,70 +516,72 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
self.assertEqual(si.grand_total, 5474.0)
|
self.assertEqual(si.grand_total, 5474.0)
|
||||||
|
|
||||||
def test_tax_calculation_with_item_tax_template(self):
|
def test_tax_calculation_with_item_tax_template(self):
|
||||||
|
import json
|
||||||
|
|
||||||
|
from erpnext.stock.get_item_details import get_item_details
|
||||||
|
|
||||||
|
# set tax template in item
|
||||||
|
item = frappe.get_cached_doc("Item", "_Test Item")
|
||||||
|
item.set(
|
||||||
|
"taxes",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"item_tax_template": "_Test Item Tax Template 1 - _TC",
|
||||||
|
"valid_from": add_days(nowdate(), -5),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
# create sales invoice with item
|
||||||
si = create_sales_invoice(qty=84, rate=4.6, do_not_save=True)
|
si = create_sales_invoice(qty=84, rate=4.6, do_not_save=True)
|
||||||
item_row = si.get("items")[0]
|
item_details = get_item_details(
|
||||||
|
doc=si,
|
||||||
|
args={
|
||||||
|
"item_code": item.item_code,
|
||||||
|
"company": si.company,
|
||||||
|
"doctype": "Sales Invoice",
|
||||||
|
"conversion_rate": 1.0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
tax_map = json.loads(item_details.item_tax_rate)
|
||||||
|
for tax in tax_map:
|
||||||
|
si.append(
|
||||||
|
"taxes",
|
||||||
|
{
|
||||||
|
"charge_type": "On Net Total",
|
||||||
|
"account_head": tax,
|
||||||
|
"rate": tax_map[tax],
|
||||||
|
"description": "Test",
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
si.submit()
|
||||||
|
si.load_from_db()
|
||||||
|
|
||||||
add_items = [
|
# check if correct tax values are applied from tax template
|
||||||
(54, "_Test Account Excise Duty @ 12 - _TC"),
|
self.assertEqual(si.net_total, 386.4)
|
||||||
(288, "_Test Account Excise Duty @ 15 - _TC"),
|
|
||||||
(144, "_Test Account Excise Duty @ 20 - _TC"),
|
expected_taxes = [
|
||||||
(430, "_Test Item Tax Template 1 - _TC"),
|
{
|
||||||
|
"tax_amount": 19.32,
|
||||||
|
"total": 405.72,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tax_amount": 38.64,
|
||||||
|
"total": 444.36,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tax_amount": 57.96,
|
||||||
|
"total": 502.32,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
for qty, item_tax_template in add_items:
|
|
||||||
item_row_copy = copy.deepcopy(item_row)
|
|
||||||
item_row_copy.qty = qty
|
|
||||||
item_row_copy.item_tax_template = item_tax_template
|
|
||||||
si.append("items", item_row_copy)
|
|
||||||
|
|
||||||
si.append(
|
for i in range(len(expected_taxes)):
|
||||||
"taxes",
|
for key in expected_taxes[i]:
|
||||||
{
|
self.assertEqual(expected_taxes[i][key], si.get("taxes")[i].get(key))
|
||||||
"account_head": "_Test Account Excise Duty - _TC",
|
|
||||||
"charge_type": "On Net Total",
|
|
||||||
"cost_center": "_Test Cost Center - _TC",
|
|
||||||
"description": "Excise Duty",
|
|
||||||
"doctype": "Sales Taxes and Charges",
|
|
||||||
"rate": 11,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
si.append(
|
|
||||||
"taxes",
|
|
||||||
{
|
|
||||||
"account_head": "_Test Account Education Cess - _TC",
|
|
||||||
"charge_type": "On Net Total",
|
|
||||||
"cost_center": "_Test Cost Center - _TC",
|
|
||||||
"description": "Education Cess",
|
|
||||||
"doctype": "Sales Taxes and Charges",
|
|
||||||
"rate": 0,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
si.append(
|
|
||||||
"taxes",
|
|
||||||
{
|
|
||||||
"account_head": "_Test Account S&H Education Cess - _TC",
|
|
||||||
"charge_type": "On Net Total",
|
|
||||||
"cost_center": "_Test Cost Center - _TC",
|
|
||||||
"description": "S&H Education Cess",
|
|
||||||
"doctype": "Sales Taxes and Charges",
|
|
||||||
"rate": 3,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
si.insert()
|
|
||||||
|
|
||||||
self.assertEqual(si.net_total, 4600)
|
self.assertEqual(si.get("base_total_taxes_and_charges"), 115.92)
|
||||||
|
|
||||||
self.assertEqual(si.get("taxes")[0].tax_amount, 502.41)
|
|
||||||
self.assertEqual(si.get("taxes")[0].total, 5102.41)
|
|
||||||
|
|
||||||
self.assertEqual(si.get("taxes")[1].tax_amount, 197.80)
|
|
||||||
self.assertEqual(si.get("taxes")[1].total, 5300.21)
|
|
||||||
|
|
||||||
self.assertEqual(si.get("taxes")[2].tax_amount, 375.36)
|
|
||||||
self.assertEqual(si.get("taxes")[2].total, 5675.57)
|
|
||||||
|
|
||||||
self.assertEqual(si.grand_total, 5675.57)
|
|
||||||
self.assertEqual(si.rounding_adjustment, 0.43)
|
|
||||||
self.assertEqual(si.rounded_total, 5676.0)
|
|
||||||
|
|
||||||
def test_tax_calculation_with_multiple_items_and_discount(self):
|
def test_tax_calculation_with_multiple_items_and_discount(self):
|
||||||
si = create_sales_invoice(qty=1, rate=75, do_not_save=True)
|
si = create_sales_invoice(qty=1, rate=75, do_not_save=True)
|
||||||
@ -1300,6 +1307,7 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
dn.submit()
|
dn.submit()
|
||||||
return dn
|
return dn
|
||||||
|
|
||||||
|
@change_settings("Accounts Settings", {"unlink_payment_on_cancellation_of_invoice": 1})
|
||||||
def test_sales_invoice_with_advance(self):
|
def test_sales_invoice_with_advance(self):
|
||||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import (
|
from erpnext.accounts.doctype.journal_entry.test_journal_entry import (
|
||||||
test_records as jv_test_records,
|
test_records as jv_test_records,
|
||||||
@ -2492,12 +2500,6 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
"stock_received_but_not_billed",
|
"stock_received_but_not_billed",
|
||||||
"Stock Received But Not Billed - _TC1",
|
"Stock Received But Not Billed - _TC1",
|
||||||
)
|
)
|
||||||
frappe.db.set_value(
|
|
||||||
"Company",
|
|
||||||
"_Test Company 1",
|
|
||||||
"expenses_included_in_valuation",
|
|
||||||
"Expenses Included In Valuation - _TC1",
|
|
||||||
)
|
|
||||||
|
|
||||||
# begin test
|
# begin test
|
||||||
si = create_sales_invoice(
|
si = create_sales_invoice(
|
||||||
@ -2547,6 +2549,7 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
si = frappe.copy_doc(test_records[0])
|
si = frappe.copy_doc(test_records[0])
|
||||||
|
si.customer = "_Test Internal Customer 3"
|
||||||
si.update_stock = 1
|
si.update_stock = 1
|
||||||
si.set_warehouse = "Finished Goods - _TC"
|
si.set_warehouse = "Finished Goods - _TC"
|
||||||
si.set_target_warehouse = "Stores - _TC"
|
si.set_target_warehouse = "Stores - _TC"
|
||||||
@ -2775,6 +2778,13 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
company="_Test Company",
|
company="_Test Company",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
tds_payable_account = create_account(
|
||||||
|
account_name="TDS Payable",
|
||||||
|
account_type="Tax",
|
||||||
|
parent_account="Duties and Taxes - _TC",
|
||||||
|
company="_Test Company",
|
||||||
|
)
|
||||||
|
|
||||||
si = create_sales_invoice(parent_cost_center="Main - _TC", do_not_save=1)
|
si = create_sales_invoice(parent_cost_center="Main - _TC", do_not_save=1)
|
||||||
si.apply_discount_on = "Grand Total"
|
si.apply_discount_on = "Grand Total"
|
||||||
si.additional_discount_account = additional_discount_account
|
si.additional_discount_account = additional_discount_account
|
||||||
@ -3073,8 +3083,8 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
si.commission_rate = commission_rate
|
si.commission_rate = commission_rate
|
||||||
self.assertRaises(frappe.ValidationError, si.save)
|
self.assertRaises(frappe.ValidationError, si.save)
|
||||||
|
|
||||||
|
@change_settings("Accounts Settings", {"acc_frozen_upto": add_days(getdate(), 1)})
|
||||||
def test_sales_invoice_submission_post_account_freezing_date(self):
|
def test_sales_invoice_submission_post_account_freezing_date(self):
|
||||||
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", add_days(getdate(), 1))
|
|
||||||
si = create_sales_invoice(do_not_save=True)
|
si = create_sales_invoice(do_not_save=True)
|
||||||
si.posting_date = add_days(getdate(), 1)
|
si.posting_date = add_days(getdate(), 1)
|
||||||
si.save()
|
si.save()
|
||||||
@ -3083,8 +3093,6 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
si.posting_date = getdate()
|
si.posting_date = getdate()
|
||||||
si.submit()
|
si.submit()
|
||||||
|
|
||||||
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
|
|
||||||
|
|
||||||
def test_over_billing_case_against_delivery_note(self):
|
def test_over_billing_case_against_delivery_note(self):
|
||||||
"""
|
"""
|
||||||
Test a case where duplicating the item with qty = 1 in the invoice
|
Test a case where duplicating the item with qty = 1 in the invoice
|
||||||
@ -3113,6 +3121,13 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
|
|
||||||
frappe.db.set_single_value("Accounts Settings", "over_billing_allowance", over_billing_allowance)
|
frappe.db.set_single_value("Accounts Settings", "over_billing_allowance", over_billing_allowance)
|
||||||
|
|
||||||
|
@change_settings(
|
||||||
|
"Accounts Settings",
|
||||||
|
{
|
||||||
|
"book_deferred_entries_via_journal_entry": 1,
|
||||||
|
"submit_journal_entries": 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
def test_multi_currency_deferred_revenue_via_journal_entry(self):
|
def test_multi_currency_deferred_revenue_via_journal_entry(self):
|
||||||
deferred_account = create_account(
|
deferred_account = create_account(
|
||||||
account_name="Deferred Revenue",
|
account_name="Deferred Revenue",
|
||||||
@ -3120,11 +3135,6 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
company="_Test Company",
|
company="_Test Company",
|
||||||
)
|
)
|
||||||
|
|
||||||
acc_settings = frappe.get_single("Accounts Settings")
|
|
||||||
acc_settings.book_deferred_entries_via_journal_entry = 1
|
|
||||||
acc_settings.submit_journal_entries = 1
|
|
||||||
acc_settings.save()
|
|
||||||
|
|
||||||
item = create_item("_Test Item for Deferred Accounting")
|
item = create_item("_Test Item for Deferred Accounting")
|
||||||
item.enable_deferred_expense = 1
|
item.enable_deferred_expense = 1
|
||||||
item.item_defaults[0].deferred_revenue_account = deferred_account
|
item.item_defaults[0].deferred_revenue_account = deferred_account
|
||||||
@ -3190,13 +3200,6 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
self.assertEqual(expected_gle[i][2], gle.debit)
|
self.assertEqual(expected_gle[i][2], gle.debit)
|
||||||
self.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
|
self.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
|
||||||
|
|
||||||
acc_settings = frappe.get_single("Accounts Settings")
|
|
||||||
acc_settings.book_deferred_entries_via_journal_entry = 0
|
|
||||||
acc_settings.submit_journal_entries = 0
|
|
||||||
acc_settings.save()
|
|
||||||
|
|
||||||
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
|
|
||||||
|
|
||||||
def test_standalone_serial_no_return(self):
|
def test_standalone_serial_no_return(self):
|
||||||
si = create_sales_invoice(
|
si = create_sales_invoice(
|
||||||
item_code="_Test Serialized Item With Series", update_stock=True, is_return=True, qty=-1
|
item_code="_Test Serialized Item With Series", update_stock=True, is_return=True, qty=-1
|
||||||
@ -3690,6 +3693,20 @@ def create_internal_parties():
|
|||||||
allowed_to_interact_with="_Test Company with perpetual inventory",
|
allowed_to_interact_with="_Test Company with perpetual inventory",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
create_internal_customer(
|
||||||
|
customer_name="_Test Internal Customer 3",
|
||||||
|
represents_company="_Test Company",
|
||||||
|
allowed_to_interact_with="_Test Company",
|
||||||
|
)
|
||||||
|
|
||||||
|
account = create_account(
|
||||||
|
account_name="Unrealized Profit",
|
||||||
|
parent_account="Current Liabilities - _TC",
|
||||||
|
company="_Test Company",
|
||||||
|
)
|
||||||
|
|
||||||
|
frappe.db.set_value("Company", "_Test Company", "unrealized_profit_loss_account", account)
|
||||||
|
|
||||||
create_internal_supplier(
|
create_internal_supplier(
|
||||||
supplier_name="_Test Internal Supplier",
|
supplier_name="_Test Internal Supplier",
|
||||||
represents_company="Wind Power LLC",
|
represents_company="Wind Power LLC",
|
||||||
|
@ -18,6 +18,14 @@ frappe.ui.form.on('Subscription', {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frm.set_query('sales_tax_template', function () {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
company: frm.doc.company
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
refresh: function (frm) {
|
refresh: function (frm) {
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe.tests.utils import FrappeTestCase
|
||||||
from frappe.utils.data import (
|
from frappe.utils.data import (
|
||||||
add_days,
|
add_days,
|
||||||
add_months,
|
add_months,
|
||||||
@ -21,11 +22,15 @@ from erpnext.accounts.doctype.subscription.subscription import get_prorata_facto
|
|||||||
test_dependencies = ("UOM", "Item Group", "Item")
|
test_dependencies = ("UOM", "Item Group", "Item")
|
||||||
|
|
||||||
|
|
||||||
class TestSubscription(unittest.TestCase):
|
class TestSubscription(FrappeTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
make_plans()
|
make_plans()
|
||||||
create_parties()
|
create_parties()
|
||||||
reset_settings()
|
reset_settings()
|
||||||
|
frappe.db.set_single_value("Accounts Settings", "acc_frozen_upto", None)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
frappe.db.rollback()
|
||||||
|
|
||||||
def test_create_subscription_with_trial_with_correct_period(self):
|
def test_create_subscription_with_trial_with_correct_period(self):
|
||||||
subscription = create_subscription(
|
subscription = create_subscription(
|
||||||
|
@ -22,7 +22,7 @@ class SubscriptionPlan(Document):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_plan_rate(
|
def get_plan_rate(
|
||||||
plan, quantity=1, customer=None, start_date=None, end_date=None, prorate_factor=1
|
plan, quantity=1, customer=None, start_date=None, end_date=None, prorate_factor=1, party=None
|
||||||
):
|
):
|
||||||
plan = frappe.get_doc("Subscription Plan", plan)
|
plan = frappe.get_doc("Subscription Plan", plan)
|
||||||
if plan.price_determination == "Fixed Rate":
|
if plan.price_determination == "Fixed Rate":
|
||||||
@ -40,6 +40,7 @@ def get_plan_rate(
|
|||||||
customer_group=customer_group,
|
customer_group=customer_group,
|
||||||
company=None,
|
company=None,
|
||||||
qty=quantity,
|
qty=quantity,
|
||||||
|
party=party,
|
||||||
)
|
)
|
||||||
if not price:
|
if not price:
|
||||||
return 0
|
return 0
|
||||||
|
@ -8,7 +8,7 @@ import frappe
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.contacts.doctype.address.address import get_default_address
|
from frappe.contacts.doctype.address.address import get_default_address
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import cint, cstr
|
from frappe.utils import cstr
|
||||||
from frappe.utils.nestedset import get_root_of
|
from frappe.utils.nestedset import get_root_of
|
||||||
|
|
||||||
from erpnext.setup.doctype.customer_group.customer_group import get_parent_customer_groups
|
from erpnext.setup.doctype.customer_group.customer_group import get_parent_customer_groups
|
||||||
@ -34,7 +34,6 @@ class TaxRule(Document):
|
|||||||
self.validate_tax_template()
|
self.validate_tax_template()
|
||||||
self.validate_from_to_dates("from_date", "to_date")
|
self.validate_from_to_dates("from_date", "to_date")
|
||||||
self.validate_filters()
|
self.validate_filters()
|
||||||
self.validate_use_for_shopping_cart()
|
|
||||||
|
|
||||||
def validate_tax_template(self):
|
def validate_tax_template(self):
|
||||||
if self.tax_type == "Sales":
|
if self.tax_type == "Sales":
|
||||||
@ -106,21 +105,6 @@ class TaxRule(Document):
|
|||||||
if tax_rule[0].priority == self.priority:
|
if tax_rule[0].priority == self.priority:
|
||||||
frappe.throw(_("Tax Rule Conflicts with {0}").format(tax_rule[0].name), ConflictingTaxRule)
|
frappe.throw(_("Tax Rule Conflicts with {0}").format(tax_rule[0].name), ConflictingTaxRule)
|
||||||
|
|
||||||
def validate_use_for_shopping_cart(self):
|
|
||||||
"""If shopping cart is enabled and no tax rule exists for shopping cart, enable this one"""
|
|
||||||
if (
|
|
||||||
not self.use_for_shopping_cart
|
|
||||||
and cint(frappe.db.get_single_value("E Commerce Settings", "enabled"))
|
|
||||||
and not frappe.db.get_value("Tax Rule", {"use_for_shopping_cart": 1, "name": ["!=", self.name]})
|
|
||||||
):
|
|
||||||
|
|
||||||
self.use_for_shopping_cart = 1
|
|
||||||
frappe.msgprint(
|
|
||||||
_(
|
|
||||||
"Enabling 'Use for Shopping Cart', as Shopping Cart is enabled and there should be at least one Tax Rule for Shopping Cart"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_party_details(party, party_type, args=None):
|
def get_party_details(party, party_type, args=None):
|
||||||
|
@ -41,7 +41,7 @@ def make_gl_entries(
|
|||||||
from_repost=from_repost,
|
from_repost=from_repost,
|
||||||
)
|
)
|
||||||
save_entries(gl_map, adv_adj, update_outstanding, from_repost)
|
save_entries(gl_map, adv_adj, update_outstanding, from_repost)
|
||||||
# Post GL Map proccess there may no be any GL Entries
|
# Post GL Map process there may no be any GL Entries
|
||||||
elif gl_map:
|
elif gl_map:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_(
|
_(
|
||||||
|
@ -5,13 +5,8 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, msgprint, scrub
|
from frappe import _, msgprint, qb, scrub
|
||||||
from frappe.contacts.doctype.address.address import (
|
from frappe.contacts.doctype.address.address import get_company_address, get_default_address
|
||||||
get_address_display,
|
|
||||||
get_company_address,
|
|
||||||
get_default_address,
|
|
||||||
)
|
|
||||||
from frappe.contacts.doctype.contact.contact import get_contact_details
|
|
||||||
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
|
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
|
||||||
from frappe.model.utils import get_fetch_values
|
from frappe.model.utils import get_fetch_values
|
||||||
from frappe.query_builder.functions import Abs, Date, Sum
|
from frappe.query_builder.functions import Abs, Date, Sum
|
||||||
@ -133,6 +128,7 @@ def _get_party_details(
|
|||||||
party_address,
|
party_address,
|
||||||
company_address,
|
company_address,
|
||||||
shipping_address,
|
shipping_address,
|
||||||
|
ignore_permissions=ignore_permissions,
|
||||||
)
|
)
|
||||||
set_contact_details(party_details, party, party_type)
|
set_contact_details(party_details, party, party_type)
|
||||||
set_other_values(party_details, party, party_type)
|
set_other_values(party_details, party, party_type)
|
||||||
@ -193,6 +189,8 @@ def set_address_details(
|
|||||||
party_address=None,
|
party_address=None,
|
||||||
company_address=None,
|
company_address=None,
|
||||||
shipping_address=None,
|
shipping_address=None,
|
||||||
|
*,
|
||||||
|
ignore_permissions=False
|
||||||
):
|
):
|
||||||
billing_address_field = (
|
billing_address_field = (
|
||||||
"customer_address" if party_type == "Lead" else party_type.lower() + "_address"
|
"customer_address" if party_type == "Lead" else party_type.lower() + "_address"
|
||||||
@ -205,13 +203,17 @@ def set_address_details(
|
|||||||
get_fetch_values(doctype, billing_address_field, party_details[billing_address_field])
|
get_fetch_values(doctype, billing_address_field, party_details[billing_address_field])
|
||||||
)
|
)
|
||||||
# address display
|
# 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
|
# shipping address
|
||||||
if party_type in ["Customer", "Lead"]:
|
if party_type in ["Customer", "Lead"]:
|
||||||
party_details.shipping_address_name = shipping_address or get_party_shipping_address(
|
party_details.shipping_address_name = shipping_address or get_party_shipping_address(
|
||||||
party_type, party.name
|
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:
|
if doctype:
|
||||||
party_details.update(
|
party_details.update(
|
||||||
get_fetch_values(doctype, "shipping_address_name", party_details.shipping_address_name)
|
get_fetch_values(doctype, "shipping_address_name", party_details.shipping_address_name)
|
||||||
@ -229,7 +231,7 @@ def set_address_details(
|
|||||||
if shipping_address:
|
if shipping_address:
|
||||||
party_details.update(
|
party_details.update(
|
||||||
shipping_address=shipping_address,
|
shipping_address=shipping_address,
|
||||||
shipping_address_display=get_address_display(shipping_address),
|
shipping_address_display=render_address(shipping_address),
|
||||||
**get_fetch_values(doctype, "shipping_address", shipping_address)
|
**get_fetch_values(doctype, "shipping_address", shipping_address)
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -238,7 +240,8 @@ def set_address_details(
|
|||||||
party_details.update(
|
party_details.update(
|
||||||
billing_address=party_details.company_address,
|
billing_address=party_details.company_address,
|
||||||
billing_address_display=(
|
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=False)
|
||||||
),
|
),
|
||||||
**get_fetch_values(doctype, "billing_address", party_details.company_address)
|
**get_fetch_values(doctype, "billing_address", party_details.company_address)
|
||||||
)
|
)
|
||||||
@ -290,7 +293,21 @@ def set_contact_details(party_details, party, party_type):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
party_details.update(get_contact_details(party_details.contact_person))
|
fields = [
|
||||||
|
"name as contact_person",
|
||||||
|
"full_name as contact_display",
|
||||||
|
"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
|
||||||
|
)
|
||||||
|
|
||||||
|
party_details.update(contact_details)
|
||||||
|
|
||||||
|
|
||||||
def set_other_values(party_details, party, party_type):
|
def set_other_values(party_details, party, party_type):
|
||||||
@ -463,11 +480,19 @@ def get_party_account_currency(party_type, party, company):
|
|||||||
|
|
||||||
def get_party_gle_currency(party_type, party, company):
|
def get_party_gle_currency(party_type, party, company):
|
||||||
def generator():
|
def generator():
|
||||||
existing_gle_currency = frappe.db.sql(
|
gl = qb.DocType("GL Entry")
|
||||||
"""select account_currency from `tabGL Entry`
|
existing_gle_currency = (
|
||||||
where docstatus=1 and company=%(company)s and party_type=%(party_type)s and party=%(party)s
|
qb.from_(gl)
|
||||||
limit 1""",
|
.select(gl.account_currency)
|
||||||
{"company": company, "party_type": party_type, "party": party},
|
.where(
|
||||||
|
(gl.docstatus == 1)
|
||||||
|
& (gl.company == company)
|
||||||
|
& (gl.party_type == party_type)
|
||||||
|
& (gl.party == party)
|
||||||
|
& (gl.is_cancelled == 0)
|
||||||
|
)
|
||||||
|
.limit(1)
|
||||||
|
.run()
|
||||||
)
|
)
|
||||||
|
|
||||||
return existing_gle_currency[0][0] if existing_gle_currency else None
|
return existing_gle_currency[0][0] if existing_gle_currency else None
|
||||||
@ -995,3 +1020,13 @@ def add_party_account(party_type, party, company, account):
|
|||||||
doc.append("accounts", accounts)
|
doc.append("accounts", accounts)
|
||||||
|
|
||||||
doc.save()
|
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)
|
||||||
|
@ -116,7 +116,7 @@ class ReceivablePayableReport(object):
|
|||||||
# build all keys, since we want to exclude vouchers beyond the report date
|
# build all keys, since we want to exclude vouchers beyond the report date
|
||||||
for ple in self.ple_entries:
|
for ple in self.ple_entries:
|
||||||
# get the balance object for voucher_type
|
# get the balance object for voucher_type
|
||||||
key = (ple.voucher_type, ple.voucher_no, ple.party)
|
key = (ple.account, ple.voucher_type, ple.voucher_no, ple.party)
|
||||||
if not key in self.voucher_balance:
|
if not key in self.voucher_balance:
|
||||||
self.voucher_balance[key] = frappe._dict(
|
self.voucher_balance[key] = frappe._dict(
|
||||||
voucher_type=ple.voucher_type,
|
voucher_type=ple.voucher_type,
|
||||||
@ -183,7 +183,7 @@ class ReceivablePayableReport(object):
|
|||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
key = (ple.against_voucher_type, ple.against_voucher_no, ple.party)
|
key = (ple.account, ple.against_voucher_type, ple.against_voucher_no, ple.party)
|
||||||
|
|
||||||
# If payment is made against credit note
|
# If payment is made against credit note
|
||||||
# and credit note is made against a Sales Invoice
|
# and credit note is made against a Sales Invoice
|
||||||
@ -192,13 +192,13 @@ class ReceivablePayableReport(object):
|
|||||||
if ple.against_voucher_no in self.return_entries:
|
if ple.against_voucher_no in self.return_entries:
|
||||||
return_against = self.return_entries.get(ple.against_voucher_no)
|
return_against = self.return_entries.get(ple.against_voucher_no)
|
||||||
if return_against:
|
if return_against:
|
||||||
key = (ple.against_voucher_type, return_against, ple.party)
|
key = (ple.account, ple.against_voucher_type, return_against, ple.party)
|
||||||
|
|
||||||
row = self.voucher_balance.get(key)
|
row = self.voucher_balance.get(key)
|
||||||
|
|
||||||
if not row:
|
if not row:
|
||||||
# no invoice, this is an invoice / stand-alone payment / credit note
|
# no invoice, this is an invoice / stand-alone payment / credit note
|
||||||
row = self.voucher_balance.get((ple.voucher_type, ple.voucher_no, ple.party))
|
row = self.voucher_balance.get((ple.account, ple.voucher_type, ple.voucher_no, ple.party))
|
||||||
|
|
||||||
row.party_type = ple.party_type
|
row.party_type = ple.party_type
|
||||||
return row
|
return row
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe import qb
|
||||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||||
from frappe.utils import add_days, flt, getdate, today
|
from frappe.utils import add_days, flt, getdate, today
|
||||||
|
|
||||||
@ -23,29 +24,6 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
|||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
|
|
||||||
def create_usd_account(self):
|
|
||||||
name = "Debtors USD"
|
|
||||||
exists = frappe.db.get_list(
|
|
||||||
"Account", filters={"company": "_Test Company 2", "account_name": "Debtors USD"}
|
|
||||||
)
|
|
||||||
if exists:
|
|
||||||
self.debtors_usd = exists[0].name
|
|
||||||
else:
|
|
||||||
debtors = frappe.get_doc(
|
|
||||||
"Account",
|
|
||||||
frappe.db.get_list(
|
|
||||||
"Account", filters={"company": "_Test Company 2", "account_name": "Debtors"}
|
|
||||||
)[0].name,
|
|
||||||
)
|
|
||||||
|
|
||||||
debtors_usd = frappe.new_doc("Account")
|
|
||||||
debtors_usd.company = debtors.company
|
|
||||||
debtors_usd.account_name = "Debtors USD"
|
|
||||||
debtors_usd.account_currency = "USD"
|
|
||||||
debtors_usd.parent_account = debtors.parent_account
|
|
||||||
debtors_usd.account_type = debtors.account_type
|
|
||||||
self.debtors_usd = debtors_usd.save().name
|
|
||||||
|
|
||||||
def create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False):
|
def create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False):
|
||||||
frappe.set_user("Administrator")
|
frappe.set_user("Administrator")
|
||||||
si = create_sales_invoice(
|
si = create_sales_invoice(
|
||||||
@ -643,3 +621,94 @@ class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
|
|||||||
self.assertEqual(len(report[1]), 2)
|
self.assertEqual(len(report[1]), 2)
|
||||||
output_for = set([x.party for x in report[1]])
|
output_for = set([x.party for x in report[1]])
|
||||||
self.assertEqual(output_for, expected_output)
|
self.assertEqual(output_for, expected_output)
|
||||||
|
|
||||||
|
def test_report_output_if_party_is_missing(self):
|
||||||
|
acc_name = "Additional Debtors"
|
||||||
|
if not frappe.db.get_value(
|
||||||
|
"Account", filters={"account_name": acc_name, "company": self.company}
|
||||||
|
):
|
||||||
|
additional_receivable_acc = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Account",
|
||||||
|
"account_name": acc_name,
|
||||||
|
"parent_account": "Accounts Receivable - " + self.company_abbr,
|
||||||
|
"company": self.company,
|
||||||
|
"account_type": "Receivable",
|
||||||
|
}
|
||||||
|
).save()
|
||||||
|
self.debtors2 = additional_receivable_acc.name
|
||||||
|
|
||||||
|
je = frappe.new_doc("Journal Entry")
|
||||||
|
je.company = self.company
|
||||||
|
je.posting_date = today()
|
||||||
|
je.append(
|
||||||
|
"accounts",
|
||||||
|
{
|
||||||
|
"account": self.debit_to,
|
||||||
|
"party_type": "Customer",
|
||||||
|
"party": self.customer,
|
||||||
|
"debit_in_account_currency": 150,
|
||||||
|
"credit_in_account_currency": 0,
|
||||||
|
"cost_center": self.cost_center,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
je.append(
|
||||||
|
"accounts",
|
||||||
|
{
|
||||||
|
"account": self.debtors2,
|
||||||
|
"party_type": "Customer",
|
||||||
|
"party": self.customer,
|
||||||
|
"debit_in_account_currency": 200,
|
||||||
|
"credit_in_account_currency": 0,
|
||||||
|
"cost_center": self.cost_center,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
je.append(
|
||||||
|
"accounts",
|
||||||
|
{
|
||||||
|
"account": self.cash,
|
||||||
|
"debit_in_account_currency": 0,
|
||||||
|
"credit_in_account_currency": 350,
|
||||||
|
"cost_center": self.cost_center,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
je.save().submit()
|
||||||
|
|
||||||
|
# manually remove party from Payment Ledger
|
||||||
|
ple = qb.DocType("Payment Ledger Entry")
|
||||||
|
qb.update(ple).set(ple.party, None).where(ple.voucher_no == je.name).run()
|
||||||
|
|
||||||
|
filters = {
|
||||||
|
"company": self.company,
|
||||||
|
"report_date": today(),
|
||||||
|
"range1": 30,
|
||||||
|
"range2": 60,
|
||||||
|
"range3": 90,
|
||||||
|
"range4": 120,
|
||||||
|
}
|
||||||
|
|
||||||
|
report_ouput = execute(filters)[1]
|
||||||
|
expected_data = [
|
||||||
|
[self.debtors2, je.doctype, je.name, "Customer", self.customer, 200.0, 0.0, 0.0, 200.0],
|
||||||
|
[self.debit_to, je.doctype, je.name, "Customer", self.customer, 150.0, 0.0, 0.0, 150.0],
|
||||||
|
]
|
||||||
|
self.assertEqual(len(report_ouput), 2)
|
||||||
|
# fetch only required fields
|
||||||
|
report_output = [
|
||||||
|
[
|
||||||
|
x.party_account,
|
||||||
|
x.voucher_type,
|
||||||
|
x.voucher_no,
|
||||||
|
"Customer",
|
||||||
|
self.customer,
|
||||||
|
x.invoiced,
|
||||||
|
x.paid,
|
||||||
|
x.credit_note,
|
||||||
|
x.outstanding,
|
||||||
|
]
|
||||||
|
for x in report_ouput
|
||||||
|
]
|
||||||
|
# use account name to sort
|
||||||
|
# post sorting output should be [[Additional Debtors, ...], [Debtors, ...]]
|
||||||
|
report_output = sorted(report_output, key=lambda x: x[0])
|
||||||
|
self.assertEqual(expected_data, report_output)
|
||||||
|
@ -177,8 +177,8 @@ def add_solvency_ratios(
|
|||||||
return_on_equity_ratio = {"ratio": "Return on Equity Ratio"}
|
return_on_equity_ratio = {"ratio": "Return on Equity Ratio"}
|
||||||
|
|
||||||
for year in years:
|
for year in years:
|
||||||
profit_after_tax = total_income[year] + total_expense[year]
|
profit_after_tax = flt(total_income.get(year)) + flt(total_expense.get(year))
|
||||||
share_holder_fund = total_asset[year] - total_liability[year]
|
share_holder_fund = flt(total_asset.get(year)) - flt(total_liability.get(year))
|
||||||
|
|
||||||
debt_equity_ratio[year] = calculate_ratio(
|
debt_equity_ratio[year] = calculate_ratio(
|
||||||
total_liability.get(year), share_holder_fund, precision
|
total_liability.get(year), share_holder_fund, precision
|
||||||
|
@ -133,15 +133,17 @@ class General_Payment_Ledger_Comparison(object):
|
|||||||
self.gle_balances = set(val.gle) | self.gle_balances
|
self.gle_balances = set(val.gle) | self.gle_balances
|
||||||
self.ple_balances = set(val.ple) | self.ple_balances
|
self.ple_balances = set(val.ple) | self.ple_balances
|
||||||
|
|
||||||
self.diff1 = self.gle_balances.difference(self.ple_balances)
|
self.variation_in_payment_ledger = self.gle_balances.difference(self.ple_balances)
|
||||||
self.diff2 = self.ple_balances.difference(self.gle_balances)
|
self.variation_in_general_ledger = self.ple_balances.difference(self.gle_balances)
|
||||||
self.diff = frappe._dict({})
|
self.diff = frappe._dict({})
|
||||||
|
|
||||||
for x in self.diff1:
|
for x in self.variation_in_payment_ledger:
|
||||||
self.diff[(x[0], x[1], x[2], x[3])] = frappe._dict({"gl_balance": x[4]})
|
self.diff[(x[0], x[1], x[2], x[3])] = frappe._dict({"gl_balance": x[4]})
|
||||||
|
|
||||||
for x in self.diff2:
|
for x in self.variation_in_general_ledger:
|
||||||
self.diff[(x[0], x[1], x[2], x[3])].update(frappe._dict({"pl_balance": x[4]}))
|
self.diff.setdefault((x[0], x[1], x[2], x[3]), frappe._dict({"gl_balance": 0.0})).update(
|
||||||
|
frappe._dict({"pl_balance": x[4]})
|
||||||
|
)
|
||||||
|
|
||||||
def generate_data(self):
|
def generate_data(self):
|
||||||
self.data = []
|
self.data = []
|
||||||
|
@ -544,6 +544,8 @@ class GrossProfitGenerator(object):
|
|||||||
new_row.qty += flt(row.qty)
|
new_row.qty += flt(row.qty)
|
||||||
new_row.buying_amount += flt(row.buying_amount, self.currency_precision)
|
new_row.buying_amount += flt(row.buying_amount, self.currency_precision)
|
||||||
new_row.base_amount += flt(row.base_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)
|
new_row = self.set_average_rate(new_row)
|
||||||
self.grouped_data.append(new_row)
|
self.grouped_data.append(new_row)
|
||||||
|
|
||||||
|
@ -32,13 +32,6 @@ frappe.query_reports["Profitability Analysis"] = {
|
|||||||
"label": __("Accounting Dimension"),
|
"label": __("Accounting Dimension"),
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"options": "Accounting Dimension",
|
"options": "Accounting Dimension",
|
||||||
"get_query": () =>{
|
|
||||||
return {
|
|
||||||
filters: {
|
|
||||||
"disabled": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "fiscal_year",
|
"fieldname": "fiscal_year",
|
||||||
|
@ -68,7 +68,11 @@ def get_result(
|
|||||||
tax_amount += entry.credit - entry.debit
|
tax_amount += entry.credit - entry.debit
|
||||||
|
|
||||||
if net_total_map.get(name):
|
if net_total_map.get(name):
|
||||||
total_amount, grand_total, base_total = net_total_map.get(name)
|
if voucher_type == "Journal Entry" and tax_amount and rate:
|
||||||
|
# back calcalute total amount from rate and tax_amount
|
||||||
|
total_amount = grand_total = base_total = tax_amount / (rate / 100)
|
||||||
|
else:
|
||||||
|
total_amount, grand_total, base_total = net_total_map.get(name)
|
||||||
else:
|
else:
|
||||||
total_amount += entry.credit
|
total_amount += entry.credit
|
||||||
|
|
||||||
|
@ -645,7 +645,7 @@ def update_reference_in_payment_entry(
|
|||||||
"outstanding_amount": d.outstanding_amount,
|
"outstanding_amount": d.outstanding_amount,
|
||||||
"allocated_amount": d.allocated_amount,
|
"allocated_amount": d.allocated_amount,
|
||||||
"exchange_rate": d.exchange_rate if d.exchange_gain_loss else payment_entry.get_exchange_rate(),
|
"exchange_rate": d.exchange_rate if d.exchange_gain_loss else payment_entry.get_exchange_rate(),
|
||||||
"exchange_gain_loss": d.exchange_gain_loss, # only populated from invoice in case of advance allocation
|
"exchange_gain_loss": d.exchange_gain_loss,
|
||||||
"account": d.account,
|
"account": d.account,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -658,28 +658,29 @@ def update_reference_in_payment_entry(
|
|||||||
existing_row.reference_doctype, existing_row.reference_name
|
existing_row.reference_doctype, existing_row.reference_name
|
||||||
).set_total_advance_paid()
|
).set_total_advance_paid()
|
||||||
|
|
||||||
original_row = existing_row.as_dict().copy()
|
if d.allocated_amount <= existing_row.allocated_amount:
|
||||||
existing_row.update(reference_details)
|
existing_row.allocated_amount -= d.allocated_amount
|
||||||
|
|
||||||
if d.allocated_amount < original_row.allocated_amount:
|
|
||||||
new_row = payment_entry.append("references")
|
new_row = payment_entry.append("references")
|
||||||
new_row.docstatus = 1
|
new_row.docstatus = 1
|
||||||
for field in list(reference_details):
|
for field in list(reference_details):
|
||||||
new_row.set(field, original_row[field])
|
new_row.set(field, reference_details[field])
|
||||||
|
|
||||||
new_row.allocated_amount = original_row.allocated_amount - d.allocated_amount
|
|
||||||
else:
|
else:
|
||||||
new_row = payment_entry.append("references")
|
new_row = payment_entry.append("references")
|
||||||
new_row.docstatus = 1
|
new_row.docstatus = 1
|
||||||
new_row.update(reference_details)
|
new_row.update(reference_details)
|
||||||
|
|
||||||
payment_entry.flags.ignore_validate_update_after_submit = True
|
payment_entry.flags.ignore_validate_update_after_submit = True
|
||||||
|
payment_entry.clear_unallocated_reference_document_rows()
|
||||||
payment_entry.setup_party_account_field()
|
payment_entry.setup_party_account_field()
|
||||||
payment_entry.set_missing_values()
|
payment_entry.set_missing_values()
|
||||||
if not skip_ref_details_update_for_pe:
|
if not skip_ref_details_update_for_pe:
|
||||||
payment_entry.set_missing_ref_details()
|
payment_entry.set_missing_ref_details()
|
||||||
payment_entry.set_amounts()
|
payment_entry.set_amounts()
|
||||||
payment_entry.make_exchange_gain_loss_journal()
|
payment_entry.make_exchange_gain_loss_journal(
|
||||||
|
frappe._dict({"difference_posting_date": d.difference_posting_date})
|
||||||
|
)
|
||||||
|
|
||||||
if not do_not_save:
|
if not do_not_save:
|
||||||
payment_entry.save(ignore_permissions=True)
|
payment_entry.save(ignore_permissions=True)
|
||||||
|
@ -9,7 +9,6 @@ frappe.ui.form.on('Asset', {
|
|||||||
frm.set_query("item_code", function() {
|
frm.set_query("item_code", function() {
|
||||||
return {
|
return {
|
||||||
"filters": {
|
"filters": {
|
||||||
"disabled": 0,
|
|
||||||
"is_fixed_asset": 1,
|
"is_fixed_asset": 1,
|
||||||
"is_stock_item": 0
|
"is_stock_item": 0
|
||||||
}
|
}
|
||||||
@ -337,7 +336,7 @@ frappe.ui.form.on('Asset', {
|
|||||||
|
|
||||||
|
|
||||||
item_code: function(frm) {
|
item_code: function(frm) {
|
||||||
if(frm.doc.item_code && frm.doc.calculate_depreciation) {
|
if(frm.doc.item_code && frm.doc.calculate_depreciation && frm.doc.gross_purchase_amount) {
|
||||||
frm.trigger('set_finance_book');
|
frm.trigger('set_finance_book');
|
||||||
} else {
|
} else {
|
||||||
frm.set_value('finance_books', []);
|
frm.set_value('finance_books', []);
|
||||||
@ -490,7 +489,7 @@ frappe.ui.form.on('Asset', {
|
|||||||
|
|
||||||
calculate_depreciation: function(frm) {
|
calculate_depreciation: function(frm) {
|
||||||
frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation);
|
frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation);
|
||||||
if (frm.doc.item_code && frm.doc.calculate_depreciation ) {
|
if (frm.doc.item_code && frm.doc.calculate_depreciation && frm.doc.gross_purchase_amount) {
|
||||||
frm.trigger("set_finance_book");
|
frm.trigger("set_finance_book");
|
||||||
} else {
|
} else {
|
||||||
frm.set_value("finance_books", []);
|
frm.set_value("finance_books", []);
|
||||||
|
@ -221,11 +221,11 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:!(doc.is_composite_asset && !doc.capitalized_in)",
|
||||||
"fieldname": "gross_purchase_amount",
|
"fieldname": "gross_purchase_amount",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Gross Purchase Amount",
|
"label": "Gross Purchase Amount",
|
||||||
"options": "Company:company:default_currency",
|
"options": "Company:company:default_currency",
|
||||||
"read_only": 1,
|
|
||||||
"read_only_depends_on": "eval:!doc.is_existing_asset",
|
"read_only_depends_on": "eval:!doc.is_existing_asset",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
@ -399,6 +399,7 @@
|
|||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:!doc.is_composite_asset && !doc.is_existing_asset",
|
||||||
"fieldname": "purchase_receipt",
|
"fieldname": "purchase_receipt",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Purchase Receipt",
|
"label": "Purchase Receipt",
|
||||||
@ -416,6 +417,7 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval:!doc.is_composite_asset && !doc.is_existing_asset",
|
||||||
"fieldname": "purchase_invoice",
|
"fieldname": "purchase_invoice",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Purchase Invoice",
|
"label": "Purchase Invoice",
|
||||||
@ -479,10 +481,11 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"depends_on": "eval.doc.asset_quantity",
|
||||||
"fieldname": "asset_quantity",
|
"fieldname": "asset_quantity",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"label": "Asset Quantity",
|
"label": "Asset Quantity",
|
||||||
"read_only_depends_on": "eval:!doc.is_existing_asset && !doc.is_composite_asset"
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "depr_entry_posting_status",
|
"fieldname": "depr_entry_posting_status",
|
||||||
@ -562,9 +565,14 @@
|
|||||||
"link_doctype": "Journal Entry",
|
"link_doctype": "Journal Entry",
|
||||||
"link_fieldname": "reference_name",
|
"link_fieldname": "reference_name",
|
||||||
"table_fieldname": "accounts"
|
"table_fieldname": "accounts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group": "Asset Capitalization",
|
||||||
|
"link_doctype": "Asset Capitalization",
|
||||||
|
"link_fieldname": "target_asset"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2023-10-03 23:28:26.732269",
|
"modified": "2023-10-27 17:03:46.629617",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Assets",
|
"module": "Assets",
|
||||||
"name": "Asset",
|
"name": "Asset",
|
||||||
@ -608,4 +616,4 @@
|
|||||||
"states": [],
|
"states": [],
|
||||||
"title_field": "asset_name",
|
"title_field": "asset_name",
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
@ -19,7 +19,6 @@ from frappe.utils import (
|
|||||||
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
|
||||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||||
from erpnext.assets.doctype.asset.asset import (
|
from erpnext.assets.doctype.asset.asset import (
|
||||||
get_asset_value_after_depreciation,
|
|
||||||
make_sales_invoice,
|
make_sales_invoice,
|
||||||
split_asset,
|
split_asset,
|
||||||
update_maintenance_status,
|
update_maintenance_status,
|
||||||
@ -194,6 +193,7 @@ class TestAsset(AssetSetup):
|
|||||||
def test_is_fixed_asset_set(self):
|
def test_is_fixed_asset_set(self):
|
||||||
asset = create_asset(is_existing_asset=1)
|
asset = create_asset(is_existing_asset=1)
|
||||||
doc = frappe.new_doc("Purchase Invoice")
|
doc = frappe.new_doc("Purchase Invoice")
|
||||||
|
doc.company = "_Test Company"
|
||||||
doc.supplier = "_Test Supplier"
|
doc.supplier = "_Test Supplier"
|
||||||
doc.append("items", {"item_code": "Macbook Pro", "qty": 1, "asset": asset.name})
|
doc.append("items", {"item_code": "Macbook Pro", "qty": 1, "asset": asset.name})
|
||||||
|
|
||||||
@ -534,7 +534,7 @@ class TestAsset(AssetSetup):
|
|||||||
|
|
||||||
self.assertEqual("Asset Received But Not Billed - _TC", doc.items[0].expense_account)
|
self.assertEqual("Asset Received But Not Billed - _TC", doc.items[0].expense_account)
|
||||||
|
|
||||||
# CWIP: Capital Work In Progress
|
# Capital Work In Progress
|
||||||
def test_cwip_accounting(self):
|
def test_cwip_accounting(self):
|
||||||
pr = make_purchase_receipt(
|
pr = make_purchase_receipt(
|
||||||
item_code="Macbook Pro", qty=1, rate=5000, do_not_submit=True, location="Test Location"
|
item_code="Macbook Pro", qty=1, rate=5000, do_not_submit=True, location="Test Location"
|
||||||
@ -567,7 +567,8 @@ class TestAsset(AssetSetup):
|
|||||||
pr.submit()
|
pr.submit()
|
||||||
|
|
||||||
expected_gle = (
|
expected_gle = (
|
||||||
("Asset Received But Not Billed - _TC", 0.0, 5250.0),
|
("_Test Account Shipping Charges - _TC", 0.0, 250.0),
|
||||||
|
("Asset Received But Not Billed - _TC", 0.0, 5000.0),
|
||||||
("CWIP Account - _TC", 5250.0, 0.0),
|
("CWIP Account - _TC", 5250.0, 0.0),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -586,9 +587,8 @@ class TestAsset(AssetSetup):
|
|||||||
expected_gle = (
|
expected_gle = (
|
||||||
("_Test Account Service Tax - _TC", 250.0, 0.0),
|
("_Test Account Service Tax - _TC", 250.0, 0.0),
|
||||||
("_Test Account Shipping Charges - _TC", 250.0, 0.0),
|
("_Test Account Shipping Charges - _TC", 250.0, 0.0),
|
||||||
("Asset Received But Not Billed - _TC", 5250.0, 0.0),
|
("Asset Received But Not Billed - _TC", 5000.0, 0.0),
|
||||||
("Creditors - _TC", 0.0, 5500.0),
|
("Creditors - _TC", 0.0, 5500.0),
|
||||||
("Expenses Included In Asset Valuation - _TC", 0.0, 250.0),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
pi_gle = frappe.db.sql(
|
pi_gle = frappe.db.sql(
|
||||||
@ -1789,6 +1789,7 @@ def create_asset_category():
|
|||||||
"fixed_asset_account": "_Test Fixed Asset - _TC",
|
"fixed_asset_account": "_Test Fixed Asset - _TC",
|
||||||
"accumulated_depreciation_account": "_Test Accumulated Depreciations - _TC",
|
"accumulated_depreciation_account": "_Test Accumulated Depreciations - _TC",
|
||||||
"depreciation_expense_account": "_Test Depreciations - _TC",
|
"depreciation_expense_account": "_Test Depreciations - _TC",
|
||||||
|
"capital_work_in_progress_account": "CWIP Account - _TC",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
asset_category.append(
|
asset_category.append(
|
||||||
|
@ -876,12 +876,8 @@ def get_items_tagged_to_wip_composite_asset(asset):
|
|||||||
"amount",
|
"amount",
|
||||||
]
|
]
|
||||||
|
|
||||||
pi_items = frappe.get_all(
|
|
||||||
"Purchase Invoice Item", filters={"wip_composite_asset": asset}, fields=fields
|
|
||||||
)
|
|
||||||
|
|
||||||
pr_items = frappe.get_all(
|
pr_items = frappe.get_all(
|
||||||
"Purchase Receipt Item", filters={"wip_composite_asset": asset}, fields=fields
|
"Purchase Receipt Item", filters={"wip_composite_asset": asset}, fields=fields
|
||||||
)
|
)
|
||||||
|
|
||||||
return pi_items + pr_items
|
return pr_items
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
"transaction_settings_section",
|
"transaction_settings_section",
|
||||||
"po_required",
|
"po_required",
|
||||||
"pr_required",
|
"pr_required",
|
||||||
"over_order_allowance",
|
"blanket_order_allowance",
|
||||||
"column_break_12",
|
"column_break_12",
|
||||||
"maintain_same_rate",
|
"maintain_same_rate",
|
||||||
"set_landed_cost_based_on_purchase_invoice_rate",
|
"set_landed_cost_based_on_purchase_invoice_rate",
|
||||||
@ -24,6 +24,7 @@
|
|||||||
"bill_for_rejected_quantity_in_purchase_invoice",
|
"bill_for_rejected_quantity_in_purchase_invoice",
|
||||||
"disable_last_purchase_rate",
|
"disable_last_purchase_rate",
|
||||||
"show_pay_button",
|
"show_pay_button",
|
||||||
|
"use_transaction_date_exchange_rate",
|
||||||
"subcontract",
|
"subcontract",
|
||||||
"backflush_raw_materials_of_subcontract_based_on",
|
"backflush_raw_materials_of_subcontract_based_on",
|
||||||
"column_break_11",
|
"column_break_11",
|
||||||
@ -160,10 +161,17 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"description": "Percentage you are allowed to order more against the Blanket Order Quantity. For example: If you have a Blanket Order of Quantity 100 units. and your Allowance is 10% then you are allowed to order 110 units.",
|
"description": "While making Purchase Invoice from Purchase Order, use Exchange Rate on Invoice's transaction date rather than inheriting it from Purchase Order. Only applies for Purchase Invoice.",
|
||||||
"fieldname": "over_order_allowance",
|
"fieldname": "use_transaction_date_exchange_rate",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Use Transaction Date Exchange Rate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"description": "Percentage you are allowed to order beyond the Blanket Order quantity.",
|
||||||
|
"fieldname": "blanket_order_allowance",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Over Order Allowance (%)"
|
"label": "Blanket Order Allowance (%)"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-cog",
|
"icon": "fa fa-cog",
|
||||||
@ -171,7 +179,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-03-02 17:02:14.404622",
|
"modified": "2023-10-25 14:03:32.520418",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Buying Settings",
|
"name": "Buying Settings",
|
||||||
|
@ -559,6 +559,9 @@ def make_purchase_receipt(source_name, target_doc=None):
|
|||||||
"bom": "bom",
|
"bom": "bom",
|
||||||
"material_request": "material_request",
|
"material_request": "material_request",
|
||||||
"material_request_item": "material_request_item",
|
"material_request_item": "material_request_item",
|
||||||
|
"sales_order": "sales_order",
|
||||||
|
"sales_order_item": "sales_order_item",
|
||||||
|
"wip_composite_asset": "wip_composite_asset",
|
||||||
},
|
},
|
||||||
"postprocess": update_item,
|
"postprocess": update_item,
|
||||||
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty)
|
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty)
|
||||||
@ -635,6 +638,7 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
|
|||||||
"field_map": {
|
"field_map": {
|
||||||
"name": "po_detail",
|
"name": "po_detail",
|
||||||
"parent": "purchase_order",
|
"parent": "purchase_order",
|
||||||
|
"wip_composite_asset": "wip_composite_asset",
|
||||||
},
|
},
|
||||||
"postprocess": update_item,
|
"postprocess": update_item,
|
||||||
"condition": lambda doc: (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)),
|
"condition": lambda doc: (doc.base_amount == 0 or abs(doc.billed_amt) < abs(doc.amount)),
|
||||||
|
@ -86,6 +86,8 @@
|
|||||||
"billed_amt",
|
"billed_amt",
|
||||||
"accounting_details",
|
"accounting_details",
|
||||||
"expense_account",
|
"expense_account",
|
||||||
|
"column_break_fyqr",
|
||||||
|
"wip_composite_asset",
|
||||||
"manufacture_details",
|
"manufacture_details",
|
||||||
"manufacturer",
|
"manufacturer",
|
||||||
"manufacturer_part_no",
|
"manufacturer_part_no",
|
||||||
@ -896,13 +898,23 @@
|
|||||||
"fieldname": "apply_tds",
|
"fieldname": "apply_tds",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Apply TDS"
|
"label": "Apply TDS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "wip_composite_asset",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "WIP Composite Asset",
|
||||||
|
"options": "Asset"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_fyqr",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-09-13 16:22:40.825092",
|
"modified": "2023-10-27 15:50:42.655573",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Purchase Order Item",
|
"name": "Purchase Order Item",
|
||||||
@ -915,4 +927,4 @@
|
|||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
@ -8,7 +8,7 @@ def get_data():
|
|||||||
"This is based on transactions against this Supplier. See timeline below for details"
|
"This is based on transactions against this Supplier. See timeline below for details"
|
||||||
),
|
),
|
||||||
"fieldname": "supplier",
|
"fieldname": "supplier",
|
||||||
"non_standard_fieldnames": {"Payment Entry": "party_name", "Bank Account": "party"},
|
"non_standard_fieldnames": {"Payment Entry": "party", "Bank Account": "party"},
|
||||||
"transactions": [
|
"transactions": [
|
||||||
{"label": _("Procurement"), "items": ["Request for Quotation", "Supplier Quotation"]},
|
{"label": _("Procurement"), "items": ["Request for Quotation", "Supplier Quotation"]},
|
||||||
{"label": _("Orders"), "items": ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]},
|
{"label": _("Orders"), "items": ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]},
|
||||||
|
@ -334,6 +334,11 @@ def make_default_records():
|
|||||||
"variable_label": "Total Ordered",
|
"variable_label": "Total Ordered",
|
||||||
"path": "get_ordered_qty",
|
"path": "get_ordered_qty",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"param_name": "total_invoiced",
|
||||||
|
"variable_label": "Total Invoiced",
|
||||||
|
"path": "get_invoiced_qty",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
install_standing_docs = [
|
install_standing_docs = [
|
||||||
{
|
{
|
||||||
|
@ -440,6 +440,23 @@ def get_ordered_qty(scorecard):
|
|||||||
).run(as_list=True)[0][0] or 0
|
).run(as_list=True)[0][0] or 0
|
||||||
|
|
||||||
|
|
||||||
|
def get_invoiced_qty(scorecard):
|
||||||
|
"""Returns the total number of invoiced quantity (based on Purchase Invoice)"""
|
||||||
|
|
||||||
|
pi = frappe.qb.DocType("Purchase Invoice")
|
||||||
|
|
||||||
|
return (
|
||||||
|
frappe.qb.from_(pi)
|
||||||
|
.select(Sum(pi.total_qty))
|
||||||
|
.where(
|
||||||
|
(pi.supplier == scorecard.supplier)
|
||||||
|
& (pi.docstatus == 1)
|
||||||
|
& (pi.posting_date >= scorecard.get("start_date"))
|
||||||
|
& (pi.posting_date <= scorecard.get("end_date"))
|
||||||
|
)
|
||||||
|
).run(as_list=True)[0][0] or 0
|
||||||
|
|
||||||
|
|
||||||
def get_rfq_total_number(scorecard):
|
def get_rfq_total_number(scorecard):
|
||||||
"""Gets the total number of RFQs sent to supplier"""
|
"""Gets the total number of RFQs sent to supplier"""
|
||||||
supplier = frappe.get_doc("Supplier", scorecard.supplier)
|
supplier = frappe.get_doc("Supplier", scorecard.supplier)
|
||||||
|
@ -6,7 +6,7 @@ import copy
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.query_builder.functions import IfNull
|
from frappe.query_builder.functions import IfNull, Sum
|
||||||
from frappe.utils import date_diff, flt, getdate
|
from frappe.utils import date_diff, flt, getdate
|
||||||
|
|
||||||
|
|
||||||
@ -57,7 +57,7 @@ def get_data(filters):
|
|||||||
po_item.qty,
|
po_item.qty,
|
||||||
po_item.received_qty,
|
po_item.received_qty,
|
||||||
(po_item.qty - po_item.received_qty).as_("pending_qty"),
|
(po_item.qty - po_item.received_qty).as_("pending_qty"),
|
||||||
IfNull(pi_item.qty, 0).as_("billed_qty"),
|
Sum(IfNull(pi_item.qty, 0)).as_("billed_qty"),
|
||||||
po_item.base_amount.as_("amount"),
|
po_item.base_amount.as_("amount"),
|
||||||
(po_item.received_qty * po_item.base_rate).as_("received_qty_amount"),
|
(po_item.received_qty * po_item.base_rate).as_("received_qty_amount"),
|
||||||
(po_item.billed_amt * IfNull(po.conversion_rate, 1)).as_("billed_amount"),
|
(po_item.billed_amt * IfNull(po.conversion_rate, 1)).as_("billed_amount"),
|
||||||
|
@ -44,11 +44,6 @@ frappe.query_reports["Supplier Quotation Comparison"] = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
return {
|
|
||||||
filters: { "disabled": 0 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -13,6 +13,7 @@ from frappe.utils import (
|
|||||||
add_days,
|
add_days,
|
||||||
add_months,
|
add_months,
|
||||||
cint,
|
cint,
|
||||||
|
comma_and,
|
||||||
flt,
|
flt,
|
||||||
fmt_money,
|
fmt_money,
|
||||||
formatdate,
|
formatdate,
|
||||||
@ -181,6 +182,17 @@ class AccountsController(TransactionBase):
|
|||||||
self.validate_party_account_currency()
|
self.validate_party_account_currency()
|
||||||
|
|
||||||
if self.doctype in ["Purchase Invoice", "Sales Invoice"]:
|
if self.doctype in ["Purchase Invoice", "Sales Invoice"]:
|
||||||
|
if invalid_advances := [
|
||||||
|
x for x in self.advances if not x.reference_type or not x.reference_name
|
||||||
|
]:
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"Rows: {0} in {1} section are Invalid. Reference Name should point to a valid Payment Entry or Journal Entry."
|
||||||
|
).format(
|
||||||
|
frappe.bold(comma_and([x.idx for x in invalid_advances])), frappe.bold(_("Advance Payments"))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid"
|
pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid"
|
||||||
if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)):
|
if cint(self.allocate_advances_automatically) and not cint(self.get(pos_check_field)):
|
||||||
self.set_advances()
|
self.set_advances()
|
||||||
@ -572,6 +584,17 @@ class AccountsController(TransactionBase):
|
|||||||
self.currency, self.company_currency, transaction_date, args
|
self.currency, self.company_currency, transaction_date, args
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
self.currency
|
||||||
|
and buying_or_selling == "Buying"
|
||||||
|
and frappe.db.get_single_value("Buying Settings", "use_transaction_date_exchange_rate")
|
||||||
|
and self.doctype == "Purchase Invoice"
|
||||||
|
):
|
||||||
|
self.use_transaction_date_exchange_rate = True
|
||||||
|
self.conversion_rate = get_exchange_rate(
|
||||||
|
self.currency, self.company_currency, transaction_date, args
|
||||||
|
)
|
||||||
|
|
||||||
def set_missing_item_details(self, for_validate=False):
|
def set_missing_item_details(self, for_validate=False):
|
||||||
"""set missing item values"""
|
"""set missing item values"""
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
@ -1155,7 +1178,9 @@ class AccountsController(TransactionBase):
|
|||||||
self.name,
|
self.name,
|
||||||
arg.get("referenced_row"),
|
arg.get("referenced_row"),
|
||||||
):
|
):
|
||||||
posting_date = frappe.db.get_value(arg.voucher_type, arg.voucher_no, "posting_date")
|
posting_date = arg.get("difference_posting_date") or frappe.db.get_value(
|
||||||
|
arg.voucher_type, arg.voucher_no, "posting_date"
|
||||||
|
)
|
||||||
je = create_gain_loss_journal(
|
je = create_gain_loss_journal(
|
||||||
self.company,
|
self.company,
|
||||||
posting_date,
|
posting_date,
|
||||||
@ -1238,7 +1263,7 @@ class AccountsController(TransactionBase):
|
|||||||
|
|
||||||
je = create_gain_loss_journal(
|
je = create_gain_loss_journal(
|
||||||
self.company,
|
self.company,
|
||||||
self.posting_date,
|
args.get("difference_posting_date") if args else self.posting_date,
|
||||||
self.party_type,
|
self.party_type,
|
||||||
self.party,
|
self.party,
|
||||||
party_account,
|
party_account,
|
||||||
|
@ -9,6 +9,8 @@ import frappe
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.utils import cstr, flt
|
from frappe.utils import cstr, flt
|
||||||
|
|
||||||
|
from erpnext.utilities.product import get_item_codes_by_attributes
|
||||||
|
|
||||||
|
|
||||||
class ItemVariantExistsError(frappe.ValidationError):
|
class ItemVariantExistsError(frappe.ValidationError):
|
||||||
pass
|
pass
|
||||||
@ -24,7 +26,8 @@ class ItemTemplateCannotHaveStock(frappe.ValidationError):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_variant(template, args=None, variant=None, manufacturer=None, manufacturer_part_no=None):
|
def get_variant(template, args=None, variant=None, manufacturer=None, manufacturer_part_no=None):
|
||||||
"""Validates Attributes and their Values, then looks for an exactly
|
"""
|
||||||
|
Validates Attributes and their Values, then looks for an exactly
|
||||||
matching Item Variant
|
matching Item Variant
|
||||||
|
|
||||||
:param item: Template Item
|
:param item: Template Item
|
||||||
@ -34,13 +37,14 @@ def get_variant(template, args=None, variant=None, manufacturer=None, manufactur
|
|||||||
|
|
||||||
if item_template.variant_based_on == "Manufacturer" and manufacturer:
|
if item_template.variant_based_on == "Manufacturer" and manufacturer:
|
||||||
return make_variant_based_on_manufacturer(item_template, manufacturer, manufacturer_part_no)
|
return make_variant_based_on_manufacturer(item_template, manufacturer, manufacturer_part_no)
|
||||||
else:
|
|
||||||
if isinstance(args, str):
|
|
||||||
args = json.loads(args)
|
|
||||||
|
|
||||||
if not args:
|
if isinstance(args, str):
|
||||||
frappe.throw(_("Please specify at least one attribute in the Attributes table"))
|
args = json.loads(args)
|
||||||
return find_variant(template, args, variant)
|
|
||||||
|
if not args:
|
||||||
|
frappe.throw(_("Please specify at least one attribute in the Attributes table"))
|
||||||
|
|
||||||
|
return find_variant(template, args, variant)
|
||||||
|
|
||||||
|
|
||||||
def make_variant_based_on_manufacturer(template, manufacturer, manufacturer_part_no):
|
def make_variant_based_on_manufacturer(template, manufacturer, manufacturer_part_no):
|
||||||
@ -157,17 +161,6 @@ def get_attribute_values(item):
|
|||||||
|
|
||||||
|
|
||||||
def find_variant(template, args, variant_item_code=None):
|
def find_variant(template, args, variant_item_code=None):
|
||||||
conditions = [
|
|
||||||
"""(iv_attribute.attribute={0} and iv_attribute.attribute_value={1})""".format(
|
|
||||||
frappe.db.escape(key), frappe.db.escape(cstr(value))
|
|
||||||
)
|
|
||||||
for key, value in args.items()
|
|
||||||
]
|
|
||||||
|
|
||||||
conditions = " or ".join(conditions)
|
|
||||||
|
|
||||||
from erpnext.e_commerce.variant_selector.utils import get_item_codes_by_attributes
|
|
||||||
|
|
||||||
possible_variants = [
|
possible_variants = [
|
||||||
i for i in get_item_codes_by_attributes(args, template) if i != variant_item_code
|
i for i in get_item_codes_by_attributes(args, template) if i != variant_item_code
|
||||||
]
|
]
|
||||||
|
@ -4,9 +4,9 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, bold, throw
|
from frappe import _, bold, throw
|
||||||
from frappe.contacts.doctype.address.address import get_address_display
|
|
||||||
from frappe.utils import cint, flt, get_link_to_form, nowtime
|
from frappe.utils import cint, 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.accounts_controller import get_taxes_and_charges
|
||||||
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
|
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
|
||||||
from erpnext.controllers.stock_controller import StockController
|
from erpnext.controllers.stock_controller import StockController
|
||||||
@ -602,7 +602,9 @@ class SellingController(StockController):
|
|||||||
|
|
||||||
for address_field, address_display_field in address_dict.items():
|
for address_field, address_display_field in address_dict.items():
|
||||||
if self.get(address_field):
|
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):
|
def validate_for_duplicate_items(self):
|
||||||
check_list, chk_dupl_itm = [], []
|
check_list, chk_dupl_itm = [], []
|
||||||
|
@ -62,9 +62,12 @@ class StockController(AccountsController):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
is_asset_pr = any(d.get("is_fixed_asset") for d in self.get("items"))
|
||||||
|
|
||||||
if (
|
if (
|
||||||
cint(erpnext.is_perpetual_inventory_enabled(self.company))
|
cint(erpnext.is_perpetual_inventory_enabled(self.company))
|
||||||
or provisional_accounting_for_non_stock_items
|
or provisional_accounting_for_non_stock_items
|
||||||
|
or is_asset_pr
|
||||||
):
|
):
|
||||||
warehouse_account = get_warehouse_account_map(self.company)
|
warehouse_account = get_warehouse_account_map(self.company)
|
||||||
|
|
||||||
@ -73,11 +76,6 @@ class StockController(AccountsController):
|
|||||||
gl_entries = self.get_gl_entries(warehouse_account)
|
gl_entries = self.get_gl_entries(warehouse_account)
|
||||||
make_gl_entries(gl_entries, from_repost=from_repost)
|
make_gl_entries(gl_entries, from_repost=from_repost)
|
||||||
|
|
||||||
elif self.doctype in ["Purchase Receipt", "Purchase Invoice"] and self.docstatus == 1:
|
|
||||||
gl_entries = []
|
|
||||||
gl_entries = self.get_asset_gl_entry(gl_entries)
|
|
||||||
make_gl_entries(gl_entries, from_repost=from_repost)
|
|
||||||
|
|
||||||
def validate_serialized_batch(self):
|
def validate_serialized_batch(self):
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
|
|
||||||
@ -692,13 +690,21 @@ class StockController(AccountsController):
|
|||||||
d.stock_uom_rate = d.rate / (d.conversion_factor or 1)
|
d.stock_uom_rate = d.rate / (d.conversion_factor or 1)
|
||||||
|
|
||||||
def validate_internal_transfer(self):
|
def validate_internal_transfer(self):
|
||||||
if (
|
if self.doctype in ("Sales Invoice", "Delivery Note", "Purchase Invoice", "Purchase Receipt"):
|
||||||
self.doctype in ("Sales Invoice", "Delivery Note", "Purchase Invoice", "Purchase Receipt")
|
if self.is_internal_transfer():
|
||||||
and self.is_internal_transfer()
|
self.validate_in_transit_warehouses()
|
||||||
):
|
self.validate_multi_currency()
|
||||||
self.validate_in_transit_warehouses()
|
self.validate_packed_items()
|
||||||
self.validate_multi_currency()
|
else:
|
||||||
self.validate_packed_items()
|
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):
|
def validate_in_transit_warehouses(self):
|
||||||
if (
|
if (
|
||||||
@ -855,8 +861,9 @@ class StockController(AccountsController):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def show_accounting_ledger_preview(company, doctype, docname):
|
def show_accounting_ledger_preview(company, doctype, docname):
|
||||||
filters = {"company": company, "include_dimensions": 1}
|
filters = frappe._dict(company=company, include_dimensions=1)
|
||||||
doc = frappe.get_doc(doctype, docname)
|
doc = frappe.get_doc(doctype, docname)
|
||||||
|
doc.run_method("before_gl_preview")
|
||||||
|
|
||||||
gl_columns, gl_data = get_accounting_ledger_preview(doc, filters)
|
gl_columns, gl_data = get_accounting_ledger_preview(doc, filters)
|
||||||
|
|
||||||
@ -867,8 +874,9 @@ def show_accounting_ledger_preview(company, doctype, docname):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def show_stock_ledger_preview(company, doctype, docname):
|
def show_stock_ledger_preview(company, doctype, docname):
|
||||||
filters = {"company": company}
|
filters = frappe._dict(company=company)
|
||||||
doc = frappe.get_doc(doctype, docname)
|
doc = frappe.get_doc(doctype, docname)
|
||||||
|
doc.run_method("before_sl_preview")
|
||||||
|
|
||||||
sl_columns, sl_data = get_stock_ledger_preview(doc, filters)
|
sl_columns, sl_data = get_stock_ledger_preview(doc, filters)
|
||||||
|
|
||||||
|
@ -25,6 +25,9 @@ class calculate_taxes_and_totals(object):
|
|||||||
def __init__(self, doc: Document):
|
def __init__(self, doc: Document):
|
||||||
self.doc = doc
|
self.doc = doc
|
||||||
frappe.flags.round_off_applicable_accounts = []
|
frappe.flags.round_off_applicable_accounts = []
|
||||||
|
frappe.flags.round_row_wise_tax = frappe.db.get_single_value(
|
||||||
|
"Accounts Settings", "round_row_wise_tax"
|
||||||
|
)
|
||||||
|
|
||||||
self._items = self.filter_rows() if self.doc.doctype == "Quotation" else self.doc.get("items")
|
self._items = self.filter_rows() if self.doc.doctype == "Quotation" else self.doc.get("items")
|
||||||
|
|
||||||
@ -370,6 +373,8 @@ class calculate_taxes_and_totals(object):
|
|||||||
for i, tax in enumerate(self.doc.get("taxes")):
|
for i, tax in enumerate(self.doc.get("taxes")):
|
||||||
# tax_amount represents the amount of tax for the current step
|
# tax_amount represents the amount of tax for the current step
|
||||||
current_tax_amount = self.get_current_tax_amount(item, tax, item_tax_map)
|
current_tax_amount = self.get_current_tax_amount(item, tax, item_tax_map)
|
||||||
|
if frappe.flags.round_row_wise_tax:
|
||||||
|
current_tax_amount = flt(current_tax_amount, tax.precision("tax_amount"))
|
||||||
|
|
||||||
# Adjust divisional loss to the last item
|
# Adjust divisional loss to the last item
|
||||||
if tax.charge_type == "Actual":
|
if tax.charge_type == "Actual":
|
||||||
@ -480,10 +485,19 @@ class calculate_taxes_and_totals(object):
|
|||||||
# store tax breakup for each item
|
# store tax breakup for each item
|
||||||
key = item.item_code or item.item_name
|
key = item.item_code or item.item_name
|
||||||
item_wise_tax_amount = current_tax_amount * self.doc.conversion_rate
|
item_wise_tax_amount = current_tax_amount * self.doc.conversion_rate
|
||||||
if tax.item_wise_tax_detail.get(key):
|
if frappe.flags.round_row_wise_tax:
|
||||||
item_wise_tax_amount += tax.item_wise_tax_detail[key][1]
|
item_wise_tax_amount = flt(item_wise_tax_amount, tax.precision("tax_amount"))
|
||||||
|
if tax.item_wise_tax_detail.get(key):
|
||||||
|
item_wise_tax_amount += flt(tax.item_wise_tax_detail[key][1], tax.precision("tax_amount"))
|
||||||
|
tax.item_wise_tax_detail[key] = [
|
||||||
|
tax_rate,
|
||||||
|
flt(item_wise_tax_amount, tax.precision("tax_amount")),
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
if tax.item_wise_tax_detail.get(key):
|
||||||
|
item_wise_tax_amount += tax.item_wise_tax_detail[key][1]
|
||||||
|
|
||||||
tax.item_wise_tax_detail[key] = [tax_rate, flt(item_wise_tax_amount)]
|
tax.item_wise_tax_detail[key] = [tax_rate, flt(item_wise_tax_amount)]
|
||||||
|
|
||||||
def round_off_totals(self, tax):
|
def round_off_totals(self, tax):
|
||||||
if tax.account_head in frappe.flags.round_off_applicable_accounts:
|
if tax.account_head in frappe.flags.round_off_applicable_accounts:
|
||||||
|
@ -7,7 +7,7 @@ import frappe
|
|||||||
from frappe import qb
|
from frappe import qb
|
||||||
from frappe.query_builder.functions import Sum
|
from frappe.query_builder.functions import Sum
|
||||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||||
from frappe.utils import add_days, flt, nowdate
|
from frappe.utils import add_days, flt, getdate, nowdate
|
||||||
|
|
||||||
from erpnext import get_default_cost_center
|
from erpnext import get_default_cost_center
|
||||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||||
@ -614,6 +614,73 @@ class TestAccountsController(FrappeTestCase):
|
|||||||
self.assertEqual(exc_je_for_si, [])
|
self.assertEqual(exc_je_for_si, [])
|
||||||
self.assertEqual(exc_je_for_pe, [])
|
self.assertEqual(exc_je_for_pe, [])
|
||||||
|
|
||||||
|
def test_15_gain_loss_on_different_posting_date(self):
|
||||||
|
# Invoice in Foreign Currency
|
||||||
|
si = self.create_sales_invoice(
|
||||||
|
posting_date=add_days(nowdate(), -2), qty=2, conversion_rate=80, rate=1
|
||||||
|
)
|
||||||
|
# Payment
|
||||||
|
pe = (
|
||||||
|
self.create_payment_entry(posting_date=add_days(nowdate(), -1), amount=2, source_exc_rate=75)
|
||||||
|
.save()
|
||||||
|
.submit()
|
||||||
|
)
|
||||||
|
|
||||||
|
# There should be outstanding in both currencies
|
||||||
|
si.reload()
|
||||||
|
self.assertEqual(si.outstanding_amount, 2)
|
||||||
|
self.assert_ledger_outstanding(si.doctype, si.name, 160.0, 2.0)
|
||||||
|
|
||||||
|
# Reconcile the remaining amount
|
||||||
|
pr = frappe.get_doc("Payment Reconciliation")
|
||||||
|
pr.company = self.company
|
||||||
|
pr.party_type = "Customer"
|
||||||
|
pr.party = self.customer
|
||||||
|
pr.receivable_payable_account = self.debit_usd
|
||||||
|
pr.get_unreconciled_entries()
|
||||||
|
self.assertEqual(len(pr.invoices), 1)
|
||||||
|
self.assertEqual(len(pr.payments), 1)
|
||||||
|
invoices = [x.as_dict() for x in pr.invoices]
|
||||||
|
payments = [x.as_dict() for x in pr.payments]
|
||||||
|
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||||
|
pr.allocation[0].gain_loss_posting_date = add_days(nowdate(), 1)
|
||||||
|
pr.reconcile()
|
||||||
|
|
||||||
|
# Exchange Gain/Loss Journal should've been created.
|
||||||
|
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
|
||||||
|
exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
|
||||||
|
self.assertNotEqual(exc_je_for_si, [])
|
||||||
|
self.assertEqual(len(exc_je_for_si), 1)
|
||||||
|
self.assertEqual(len(exc_je_for_pe), 1)
|
||||||
|
self.assertEqual(exc_je_for_si[0], exc_je_for_pe[0])
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
frappe.db.get_value("Journal Entry", exc_je_for_si[0].parent, "posting_date"),
|
||||||
|
getdate(add_days(nowdate(), 1)),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(len(pr.invoices), 0)
|
||||||
|
self.assertEqual(len(pr.payments), 0)
|
||||||
|
|
||||||
|
# There should be no outstanding
|
||||||
|
si.reload()
|
||||||
|
self.assertEqual(si.outstanding_amount, 0)
|
||||||
|
self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
|
||||||
|
|
||||||
|
# Cancel Payment
|
||||||
|
pe.reload()
|
||||||
|
pe.cancel()
|
||||||
|
|
||||||
|
si.reload()
|
||||||
|
self.assertEqual(si.outstanding_amount, 2)
|
||||||
|
self.assert_ledger_outstanding(si.doctype, si.name, 160.0, 2.0)
|
||||||
|
|
||||||
|
# Exchange Gain/Loss Journal should've been cancelled
|
||||||
|
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
|
||||||
|
exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
|
||||||
|
self.assertEqual(exc_je_for_si, [])
|
||||||
|
self.assertEqual(exc_je_for_pe, [])
|
||||||
|
|
||||||
def test_20_journal_against_sales_invoice(self):
|
def test_20_journal_against_sales_invoice(self):
|
||||||
# Invoice in Foreign Currency
|
# Invoice in Foreign Currency
|
||||||
si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1)
|
si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1)
|
||||||
|
@ -1,81 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
|
||||||
# For license information, please see license.txt
|
|
||||||
|
|
||||||
import json
|
|
||||||
|
|
||||||
import frappe
|
|
||||||
from frappe.utils import cint
|
|
||||||
|
|
||||||
from erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder
|
|
||||||
from erpnext.e_commerce.product_data_engine.query import ProductQuery
|
|
||||||
from erpnext.setup.doctype.item_group.item_group import get_child_groups_for_website
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
|
||||||
def get_product_filter_data(query_args=None):
|
|
||||||
"""
|
|
||||||
Returns filtered products and discount filters.
|
|
||||||
:param query_args (dict): contains filters to get products list
|
|
||||||
|
|
||||||
Query Args filters:
|
|
||||||
search (str): Search Term.
|
|
||||||
field_filters (dict): Keys include item_group, brand, etc.
|
|
||||||
attribute_filters(dict): Keys include Color, Size, etc.
|
|
||||||
start (int): Offset items by
|
|
||||||
item_group (str): Valid Item Group
|
|
||||||
from_filters (bool): Set as True to jump to page 1
|
|
||||||
"""
|
|
||||||
if isinstance(query_args, str):
|
|
||||||
query_args = json.loads(query_args)
|
|
||||||
|
|
||||||
query_args = frappe._dict(query_args)
|
|
||||||
if query_args:
|
|
||||||
search = query_args.get("search")
|
|
||||||
field_filters = query_args.get("field_filters", {})
|
|
||||||
attribute_filters = query_args.get("attribute_filters", {})
|
|
||||||
start = cint(query_args.start) if query_args.get("start") else 0
|
|
||||||
item_group = query_args.get("item_group")
|
|
||||||
from_filters = query_args.get("from_filters")
|
|
||||||
else:
|
|
||||||
search, attribute_filters, item_group, from_filters = None, None, None, None
|
|
||||||
field_filters = {}
|
|
||||||
start = 0
|
|
||||||
|
|
||||||
# if new filter is checked, reset start to show filtered items from page 1
|
|
||||||
if from_filters:
|
|
||||||
start = 0
|
|
||||||
|
|
||||||
sub_categories = []
|
|
||||||
if item_group:
|
|
||||||
sub_categories = get_child_groups_for_website(item_group, immediate=True)
|
|
||||||
|
|
||||||
engine = ProductQuery()
|
|
||||||
try:
|
|
||||||
result = engine.query(
|
|
||||||
attribute_filters, field_filters, search_term=search, start=start, item_group=item_group
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
frappe.log_error("Product query with filter failed")
|
|
||||||
return {"exc": "Something went wrong!"}
|
|
||||||
|
|
||||||
# discount filter data
|
|
||||||
filters = {}
|
|
||||||
discounts = result["discounts"]
|
|
||||||
|
|
||||||
if discounts:
|
|
||||||
filter_engine = ProductFiltersBuilder()
|
|
||||||
filters["discount_filters"] = filter_engine.get_discount_filters(discounts)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"items": result["items"] or [],
|
|
||||||
"filters": filters,
|
|
||||||
"settings": engine.settings,
|
|
||||||
"sub_categories": sub_categories,
|
|
||||||
"items_count": result["items_count"],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
|
||||||
def get_guest_redirect_on_action():
|
|
||||||
return frappe.db.get_single_value("E Commerce Settings", "redirect_on_action")
|
|
@ -1,58 +0,0 @@
|
|||||||
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
|
||||||
// For license information, please see license.txt
|
|
||||||
|
|
||||||
frappe.ui.form.on("E Commerce Settings", {
|
|
||||||
onload: function(frm) {
|
|
||||||
if(frm.doc.__onload && frm.doc.__onload.quotation_series) {
|
|
||||||
frm.fields_dict.quotation_series.df.options = frm.doc.__onload.quotation_series;
|
|
||||||
frm.refresh_field("quotation_series");
|
|
||||||
}
|
|
||||||
|
|
||||||
frm.set_query('payment_gateway_account', function() {
|
|
||||||
return { 'filters': { 'payment_channel': "Email" } };
|
|
||||||
});
|
|
||||||
},
|
|
||||||
refresh: function(frm) {
|
|
||||||
if (frm.doc.enabled) {
|
|
||||||
frm.get_field('store_page_docs').$wrapper.removeClass('hide-control').html(
|
|
||||||
`<div>${__("Follow these steps to create a landing page for your store")}:
|
|
||||||
<a href="https://docs.erpnext.com/docs/user/manual/en/website/store-landing-page"
|
|
||||||
style="color: var(--gray-600)">
|
|
||||||
docs/store-landing-page
|
|
||||||
</a>
|
|
||||||
</div>`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
frappe.model.with_doctype("Website Item", () => {
|
|
||||||
const web_item_meta = frappe.get_meta('Website Item');
|
|
||||||
|
|
||||||
const valid_fields = web_item_meta.fields.filter(df =>
|
|
||||||
["Link", "Table MultiSelect"].includes(df.fieldtype) && !df.hidden
|
|
||||||
).map(df =>
|
|
||||||
({ label: df.label, value: df.fieldname })
|
|
||||||
);
|
|
||||||
|
|
||||||
frm.get_field("filter_fields").grid.update_docfield_property(
|
|
||||||
'fieldname', 'options', valid_fields
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
enabled: function(frm) {
|
|
||||||
if (frm.doc.enabled === 1) {
|
|
||||||
frm.set_value('enable_variants', 1);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
frm.set_value('company', '');
|
|
||||||
frm.set_value('price_list', '');
|
|
||||||
frm.set_value('default_customer_group', '');
|
|
||||||
frm.set_value('quotation_series', '');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
enable_checkout: function(frm) {
|
|
||||||
if (frm.doc.enable_checkout) {
|
|
||||||
erpnext.utils.check_payments_app();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,395 +0,0 @@
|
|||||||
{
|
|
||||||
"actions": [],
|
|
||||||
"creation": "2021-02-10 17:13:39.139103",
|
|
||||||
"doctype": "DocType",
|
|
||||||
"editable_grid": 1,
|
|
||||||
"engine": "InnoDB",
|
|
||||||
"field_order": [
|
|
||||||
"products_per_page",
|
|
||||||
"filter_categories_section",
|
|
||||||
"enable_field_filters",
|
|
||||||
"filter_fields",
|
|
||||||
"enable_attribute_filters",
|
|
||||||
"filter_attributes",
|
|
||||||
"display_settings_section",
|
|
||||||
"hide_variants",
|
|
||||||
"enable_variants",
|
|
||||||
"show_price",
|
|
||||||
"column_break_9",
|
|
||||||
"show_stock_availability",
|
|
||||||
"show_quantity_in_website",
|
|
||||||
"allow_items_not_in_stock",
|
|
||||||
"column_break_13",
|
|
||||||
"show_apply_coupon_code_in_website",
|
|
||||||
"show_contact_us_button",
|
|
||||||
"show_attachments",
|
|
||||||
"section_break_18",
|
|
||||||
"company",
|
|
||||||
"price_list",
|
|
||||||
"enabled",
|
|
||||||
"store_page_docs",
|
|
||||||
"column_break_21",
|
|
||||||
"default_customer_group",
|
|
||||||
"quotation_series",
|
|
||||||
"checkout_settings_section",
|
|
||||||
"enable_checkout",
|
|
||||||
"show_price_in_quotation",
|
|
||||||
"column_break_27",
|
|
||||||
"save_quotations_as_draft",
|
|
||||||
"payment_gateway_account",
|
|
||||||
"payment_success_url",
|
|
||||||
"add_ons_section",
|
|
||||||
"enable_wishlist",
|
|
||||||
"column_break_22",
|
|
||||||
"enable_reviews",
|
|
||||||
"column_break_23",
|
|
||||||
"enable_recommendations",
|
|
||||||
"item_search_settings_section",
|
|
||||||
"redisearch_warning",
|
|
||||||
"search_index_fields",
|
|
||||||
"is_redisearch_enabled",
|
|
||||||
"is_redisearch_loaded",
|
|
||||||
"shop_by_category_section",
|
|
||||||
"slideshow",
|
|
||||||
"guest_display_settings_section",
|
|
||||||
"hide_price_for_guest",
|
|
||||||
"redirect_on_action"
|
|
||||||
],
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"default": "6",
|
|
||||||
"fieldname": "products_per_page",
|
|
||||||
"fieldtype": "Int",
|
|
||||||
"label": "Products per Page"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"collapsible": 1,
|
|
||||||
"fieldname": "filter_categories_section",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "Filters and Categories"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fieldname": "hide_variants",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Hide Variants"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"description": "The field filters will also work as categories in the <b>Shop by Category</b> page.",
|
|
||||||
"fieldname": "enable_field_filters",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Enable Field Filters (Categories)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fieldname": "enable_attribute_filters",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Enable Attribute Filters"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "enable_field_filters",
|
|
||||||
"fieldname": "filter_fields",
|
|
||||||
"fieldtype": "Table",
|
|
||||||
"label": "Website Item Fields",
|
|
||||||
"options": "Website Filter Field"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "enable_attribute_filters",
|
|
||||||
"fieldname": "filter_attributes",
|
|
||||||
"fieldtype": "Table",
|
|
||||||
"label": "Attributes",
|
|
||||||
"options": "Website Attribute"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fieldname": "enabled",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Enable Shopping Cart"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "doc.enabled",
|
|
||||||
"fieldname": "store_page_docs",
|
|
||||||
"fieldtype": "HTML"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "display_settings_section",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "Display Settings"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fieldname": "show_attachments",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Show Public Attachments"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fieldname": "show_price",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Show Price"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fieldname": "show_stock_availability",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Show Stock Availability"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fieldname": "enable_variants",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Enable Variant Selection"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_13",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fieldname": "show_contact_us_button",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Show Contact Us Button"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"depends_on": "show_stock_availability",
|
|
||||||
"fieldname": "show_quantity_in_website",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Show Stock Quantity"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fieldname": "show_apply_coupon_code_in_website",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Show Apply Coupon Code"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fieldname": "allow_items_not_in_stock",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Allow items not in stock to be added to cart"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "section_break_18",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "Shopping Cart"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "enabled",
|
|
||||||
"fieldname": "company",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Company",
|
|
||||||
"mandatory_depends_on": "eval: doc.enabled === 1",
|
|
||||||
"options": "Company",
|
|
||||||
"remember_last_selected_value": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "enabled",
|
|
||||||
"description": "Prices will not be shown if Price List is not set",
|
|
||||||
"fieldname": "price_list",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"label": "Price List",
|
|
||||||
"mandatory_depends_on": "eval: doc.enabled === 1",
|
|
||||||
"options": "Price List"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_21",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "enabled",
|
|
||||||
"fieldname": "default_customer_group",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"ignore_user_permissions": 1,
|
|
||||||
"label": "Default Customer Group",
|
|
||||||
"mandatory_depends_on": "eval: doc.enabled === 1",
|
|
||||||
"options": "Customer Group"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "enabled",
|
|
||||||
"fieldname": "quotation_series",
|
|
||||||
"fieldtype": "Select",
|
|
||||||
"label": "Quotation Series",
|
|
||||||
"mandatory_depends_on": "eval: doc.enabled === 1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"collapsible": 1,
|
|
||||||
"collapsible_depends_on": "eval:doc.enable_checkout",
|
|
||||||
"depends_on": "enabled",
|
|
||||||
"fieldname": "checkout_settings_section",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "Checkout Settings"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fieldname": "enable_checkout",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Enable Checkout"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "Orders",
|
|
||||||
"depends_on": "enable_checkout",
|
|
||||||
"description": "After payment completion redirect user to selected page.",
|
|
||||||
"fieldname": "payment_success_url",
|
|
||||||
"fieldtype": "Select",
|
|
||||||
"label": "Payment Success Url",
|
|
||||||
"mandatory_depends_on": "enable_checkout",
|
|
||||||
"options": "\nOrders\nInvoices\nMy Account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_27",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"depends_on": "eval: doc.enable_checkout == 0",
|
|
||||||
"fieldname": "save_quotations_as_draft",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Save Quotations as Draft"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "enable_checkout",
|
|
||||||
"fieldname": "payment_gateway_account",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"label": "Payment Gateway Account",
|
|
||||||
"mandatory_depends_on": "enable_checkout",
|
|
||||||
"options": "Payment Gateway Account"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"collapsible": 1,
|
|
||||||
"depends_on": "enable_field_filters",
|
|
||||||
"fieldname": "shop_by_category_section",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "Shop by Category"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "slideshow",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"label": "Slideshow",
|
|
||||||
"options": "Website Slideshow"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"collapsible": 1,
|
|
||||||
"fieldname": "add_ons_section",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "Add-ons"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fieldname": "enable_wishlist",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Enable Wishlist"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fieldname": "enable_reviews",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Enable Reviews and Ratings"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "search_index_fields",
|
|
||||||
"fieldtype": "Small Text",
|
|
||||||
"label": "Search Index Fields",
|
|
||||||
"mandatory_depends_on": "is_redisearch_enabled",
|
|
||||||
"read_only_depends_on": "eval:!doc.is_redisearch_loaded"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"collapsible": 1,
|
|
||||||
"fieldname": "item_search_settings_section",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "Item Search Settings"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fieldname": "is_redisearch_loaded",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"hidden": 1,
|
|
||||||
"label": "Is Redisearch Loaded"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "eval:!doc.is_redisearch_loaded",
|
|
||||||
"fieldname": "redisearch_warning",
|
|
||||||
"fieldtype": "HTML",
|
|
||||||
"label": "Redisearch Warning",
|
|
||||||
"options": "<p class=\"alert alert-warning\">Redisearch is not loaded. If you want to use the advanced product search feature, refer <a class=\"alert-link\" href=\"https://docs.erpnext.com/docs/v13/user/manual/en/setting-up/articles/installing-redisearch\" target=\"_blank\">here</a>.</p>"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"depends_on": "eval:doc.show_price",
|
|
||||||
"fieldname": "hide_price_for_guest",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Hide Price for Guest"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_9",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"collapsible": 1,
|
|
||||||
"fieldname": "guest_display_settings_section",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "Guest Display Settings"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Link to redirect Guest on actions that need login such as add to cart, wishlist, etc. <b>E.g.: /login</b>",
|
|
||||||
"fieldname": "redirect_on_action",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "Redirect on Action"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_22",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_23",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fieldname": "enable_recommendations",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Enable Recommendations"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"depends_on": "eval: doc.enable_checkout == 0",
|
|
||||||
"fieldname": "show_price_in_quotation",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Show Price in Quotation"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fieldname": "is_redisearch_enabled",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Enable Redisearch",
|
|
||||||
"read_only_depends_on": "eval:!doc.is_redisearch_loaded"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"index_web_pages_for_search": 1,
|
|
||||||
"issingle": 1,
|
|
||||||
"links": [],
|
|
||||||
"modified": "2022-04-01 18:35:56.106756",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"module": "E-commerce",
|
|
||||||
"name": "E Commerce Settings",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"permissions": [
|
|
||||||
{
|
|
||||||
"create": 1,
|
|
||||||
"delete": 1,
|
|
||||||
"email": 1,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
|
||||||
"role": "System Manager",
|
|
||||||
"share": 1,
|
|
||||||
"write": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"sort_field": "modified",
|
|
||||||
"sort_order": "DESC",
|
|
||||||
"states": [],
|
|
||||||
"track_changes": 1
|
|
||||||
}
|
|
@ -1,185 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
|
||||||
# For license information, please see license.txt
|
|
||||||
|
|
||||||
import frappe
|
|
||||||
from frappe import _
|
|
||||||
from frappe.model.document import Document
|
|
||||||
from frappe.utils import comma_and, flt, unique
|
|
||||||
|
|
||||||
from erpnext.e_commerce.redisearch_utils import (
|
|
||||||
create_website_items_index,
|
|
||||||
define_autocomplete_dictionary,
|
|
||||||
get_indexable_web_fields,
|
|
||||||
is_search_module_loaded,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ShoppingCartSetupError(frappe.ValidationError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ECommerceSettings(Document):
|
|
||||||
def onload(self):
|
|
||||||
self.get("__onload").quotation_series = frappe.get_meta("Quotation").get_options("naming_series")
|
|
||||||
|
|
||||||
# flag >> if redisearch is installed and loaded
|
|
||||||
self.is_redisearch_loaded = is_search_module_loaded()
|
|
||||||
|
|
||||||
def validate(self):
|
|
||||||
self.validate_field_filters(self.filter_fields, self.enable_field_filters)
|
|
||||||
self.validate_attribute_filters()
|
|
||||||
self.validate_checkout()
|
|
||||||
self.validate_search_index_fields()
|
|
||||||
|
|
||||||
if self.enabled:
|
|
||||||
self.validate_price_list_exchange_rate()
|
|
||||||
|
|
||||||
frappe.clear_document_cache("E Commerce Settings", "E Commerce Settings")
|
|
||||||
|
|
||||||
self.is_redisearch_enabled_pre_save = frappe.db.get_single_value(
|
|
||||||
"E Commerce Settings", "is_redisearch_enabled"
|
|
||||||
)
|
|
||||||
|
|
||||||
def after_save(self):
|
|
||||||
self.create_redisearch_indexes()
|
|
||||||
|
|
||||||
def create_redisearch_indexes(self):
|
|
||||||
# if redisearch is enabled (value changed) create indexes and dictionary
|
|
||||||
value_changed = self.is_redisearch_enabled != self.is_redisearch_enabled_pre_save
|
|
||||||
if self.is_redisearch_loaded and self.is_redisearch_enabled and value_changed:
|
|
||||||
define_autocomplete_dictionary()
|
|
||||||
create_website_items_index()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def validate_field_filters(filter_fields, enable_field_filters):
|
|
||||||
if not (enable_field_filters and filter_fields):
|
|
||||||
return
|
|
||||||
|
|
||||||
web_item_meta = frappe.get_meta("Website Item")
|
|
||||||
valid_fields = [
|
|
||||||
df.fieldname for df in web_item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"]
|
|
||||||
]
|
|
||||||
|
|
||||||
for row in filter_fields:
|
|
||||||
if row.fieldname not in valid_fields:
|
|
||||||
frappe.throw(
|
|
||||||
_(
|
|
||||||
"Filter Fields Row #{0}: Fieldname {1} must be of type 'Link' or 'Table MultiSelect'"
|
|
||||||
).format(row.idx, frappe.bold(row.fieldname))
|
|
||||||
)
|
|
||||||
|
|
||||||
def validate_attribute_filters(self):
|
|
||||||
if not (self.enable_attribute_filters and self.filter_attributes):
|
|
||||||
return
|
|
||||||
|
|
||||||
# if attribute filters are enabled, hide_variants should be disabled
|
|
||||||
self.hide_variants = 0
|
|
||||||
|
|
||||||
def validate_checkout(self):
|
|
||||||
if self.enable_checkout and not self.payment_gateway_account:
|
|
||||||
self.enable_checkout = 0
|
|
||||||
|
|
||||||
def validate_search_index_fields(self):
|
|
||||||
if not self.search_index_fields:
|
|
||||||
return
|
|
||||||
|
|
||||||
fields = self.search_index_fields.replace(" ", "")
|
|
||||||
fields = unique(fields.strip(",").split(",")) # Remove extra ',' and remove duplicates
|
|
||||||
|
|
||||||
# All fields should be indexable
|
|
||||||
allowed_indexable_fields = get_indexable_web_fields()
|
|
||||||
|
|
||||||
if not (set(fields).issubset(allowed_indexable_fields)):
|
|
||||||
invalid_fields = list(set(fields).difference(allowed_indexable_fields))
|
|
||||||
num_invalid_fields = len(invalid_fields)
|
|
||||||
invalid_fields = comma_and(invalid_fields)
|
|
||||||
|
|
||||||
if num_invalid_fields > 1:
|
|
||||||
frappe.throw(
|
|
||||||
_("{0} are not valid options for Search Index Field.").format(frappe.bold(invalid_fields))
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
frappe.throw(
|
|
||||||
_("{0} is not a valid option for Search Index Field.").format(frappe.bold(invalid_fields))
|
|
||||||
)
|
|
||||||
|
|
||||||
self.search_index_fields = ",".join(fields)
|
|
||||||
|
|
||||||
def validate_price_list_exchange_rate(self):
|
|
||||||
"Check if exchange rate exists for Price List currency (to Company's currency)."
|
|
||||||
from erpnext.setup.utils import get_exchange_rate
|
|
||||||
|
|
||||||
if not self.enabled or not self.company or not self.price_list:
|
|
||||||
return # this function is also called from hooks, check values again
|
|
||||||
|
|
||||||
company_currency = frappe.get_cached_value("Company", self.company, "default_currency")
|
|
||||||
price_list_currency = frappe.db.get_value("Price List", self.price_list, "currency")
|
|
||||||
|
|
||||||
if not company_currency:
|
|
||||||
msg = f"Please specify currency in Company {self.company}"
|
|
||||||
frappe.throw(_(msg), title=_("Missing Currency"), exc=ShoppingCartSetupError)
|
|
||||||
|
|
||||||
if not price_list_currency:
|
|
||||||
msg = f"Please specify currency in Price List {frappe.bold(self.price_list)}"
|
|
||||||
frappe.throw(_(msg), title=_("Missing Currency"), exc=ShoppingCartSetupError)
|
|
||||||
|
|
||||||
if price_list_currency != company_currency:
|
|
||||||
from_currency, to_currency = price_list_currency, company_currency
|
|
||||||
|
|
||||||
# Get exchange rate checks Currency Exchange Records too
|
|
||||||
exchange_rate = get_exchange_rate(from_currency, to_currency, args="for_selling")
|
|
||||||
|
|
||||||
if not flt(exchange_rate):
|
|
||||||
msg = f"Missing Currency Exchange Rates for {from_currency}-{to_currency}"
|
|
||||||
frappe.throw(_(msg), title=_("Missing"), exc=ShoppingCartSetupError)
|
|
||||||
|
|
||||||
def validate_tax_rule(self):
|
|
||||||
if not frappe.db.get_value("Tax Rule", {"use_for_shopping_cart": 1}, "name"):
|
|
||||||
frappe.throw(frappe._("Set Tax Rule for shopping cart"), ShoppingCartSetupError)
|
|
||||||
|
|
||||||
def get_tax_master(self, billing_territory):
|
|
||||||
tax_master = self.get_name_from_territory(
|
|
||||||
billing_territory, "sales_taxes_and_charges_masters", "sales_taxes_and_charges_master"
|
|
||||||
)
|
|
||||||
return tax_master and tax_master[0] or None
|
|
||||||
|
|
||||||
def get_shipping_rules(self, shipping_territory):
|
|
||||||
return self.get_name_from_territory(shipping_territory, "shipping_rules", "shipping_rule")
|
|
||||||
|
|
||||||
def on_change(self):
|
|
||||||
old_doc = self.get_doc_before_save()
|
|
||||||
|
|
||||||
if old_doc:
|
|
||||||
old_fields = old_doc.search_index_fields
|
|
||||||
new_fields = self.search_index_fields
|
|
||||||
|
|
||||||
# if search index fields get changed
|
|
||||||
if not (new_fields == old_fields):
|
|
||||||
create_website_items_index()
|
|
||||||
|
|
||||||
|
|
||||||
def validate_cart_settings(doc=None, method=None):
|
|
||||||
frappe.get_doc("E Commerce Settings", "E Commerce Settings").run_method("validate")
|
|
||||||
|
|
||||||
|
|
||||||
def get_shopping_cart_settings():
|
|
||||||
return frappe.get_cached_doc("E Commerce Settings")
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
|
||||||
def is_cart_enabled():
|
|
||||||
return get_shopping_cart_settings().enabled
|
|
||||||
|
|
||||||
|
|
||||||
def show_quantity_in_website():
|
|
||||||
return get_shopping_cart_settings().show_quantity_in_website
|
|
||||||
|
|
||||||
|
|
||||||
def check_shopping_cart_enabled():
|
|
||||||
if not get_shopping_cart_settings().enabled:
|
|
||||||
frappe.throw(_("You need to enable Shopping Cart"), ShoppingCartSetupError)
|
|
||||||
|
|
||||||
|
|
||||||
def show_attachments():
|
|
||||||
return get_shopping_cart_settings().show_attachments
|
|
@ -1,53 +0,0 @@
|
|||||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
|
||||||
# See license.txt
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import frappe
|
|
||||||
|
|
||||||
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
|
|
||||||
ShoppingCartSetupError,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestECommerceSettings(unittest.TestCase):
|
|
||||||
def tearDown(self):
|
|
||||||
frappe.db.rollback()
|
|
||||||
|
|
||||||
def test_tax_rule_validation(self):
|
|
||||||
frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 0")
|
|
||||||
frappe.db.commit() # nosemgrep
|
|
||||||
|
|
||||||
cart_settings = frappe.get_doc("E Commerce Settings")
|
|
||||||
cart_settings.enabled = 1
|
|
||||||
if not frappe.db.get_value("Tax Rule", {"use_for_shopping_cart": 1}, "name"):
|
|
||||||
self.assertRaises(ShoppingCartSetupError, cart_settings.validate_tax_rule)
|
|
||||||
|
|
||||||
frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 1")
|
|
||||||
|
|
||||||
def test_invalid_filter_fields(self):
|
|
||||||
"Check if Item fields are blocked in E Commerce Settings filter fields."
|
|
||||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
|
||||||
|
|
||||||
setup_e_commerce_settings({"enable_field_filters": 1})
|
|
||||||
|
|
||||||
create_custom_field(
|
|
||||||
"Item",
|
|
||||||
dict(owner="Administrator", fieldname="test_data", label="Test", fieldtype="Data"),
|
|
||||||
)
|
|
||||||
settings = frappe.get_doc("E Commerce Settings")
|
|
||||||
settings.append("filter_fields", {"fieldname": "test_data"})
|
|
||||||
|
|
||||||
self.assertRaises(frappe.ValidationError, settings.save)
|
|
||||||
|
|
||||||
|
|
||||||
def setup_e_commerce_settings(values_dict):
|
|
||||||
"Accepts a dict of values that updates E Commerce Settings."
|
|
||||||
if not values_dict:
|
|
||||||
return
|
|
||||||
|
|
||||||
doc = frappe.get_doc("E Commerce Settings", "E Commerce Settings")
|
|
||||||
doc.update(values_dict)
|
|
||||||
doc.save()
|
|
||||||
|
|
||||||
|
|
||||||
test_dependencies = ["Tax Rule"]
|
|
@ -1,8 +0,0 @@
|
|||||||
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
|
||||||
// For license information, please see license.txt
|
|
||||||
|
|
||||||
frappe.ui.form.on('Item Review', {
|
|
||||||
// refresh: function(frm) {
|
|
||||||
|
|
||||||
// }
|
|
||||||
});
|
|
@ -1,134 +0,0 @@
|
|||||||
{
|
|
||||||
"actions": [],
|
|
||||||
"beta": 1,
|
|
||||||
"creation": "2021-03-23 16:47:26.542226",
|
|
||||||
"doctype": "DocType",
|
|
||||||
"editable_grid": 1,
|
|
||||||
"engine": "InnoDB",
|
|
||||||
"field_order": [
|
|
||||||
"website_item",
|
|
||||||
"user",
|
|
||||||
"customer",
|
|
||||||
"column_break_3",
|
|
||||||
"item",
|
|
||||||
"published_on",
|
|
||||||
"reviews_section",
|
|
||||||
"review_title",
|
|
||||||
"rating",
|
|
||||||
"comment"
|
|
||||||
],
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldname": "website_item",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"label": "Website Item",
|
|
||||||
"options": "Website Item",
|
|
||||||
"read_only": 1,
|
|
||||||
"reqd": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "user",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "User",
|
|
||||||
"options": "User",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_3",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fetch_from": "website_item.item_code",
|
|
||||||
"fieldname": "item",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Item",
|
|
||||||
"options": "Item",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "reviews_section",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "Reviews"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "rating",
|
|
||||||
"fieldtype": "Rating",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Rating",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "comment",
|
|
||||||
"fieldtype": "Small Text",
|
|
||||||
"label": "Comment",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "review_title",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "Review Title",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "customer",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"label": "Customer",
|
|
||||||
"options": "Customer",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "published_on",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "Published on",
|
|
||||||
"read_only": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"index_web_pages_for_search": 1,
|
|
||||||
"links": [],
|
|
||||||
"modified": "2021-08-10 12:08:58.119691",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"module": "E-commerce",
|
|
||||||
"name": "Item Review",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"permissions": [
|
|
||||||
{
|
|
||||||
"create": 1,
|
|
||||||
"delete": 1,
|
|
||||||
"email": 1,
|
|
||||||
"export": 1,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
|
||||||
"report": 1,
|
|
||||||
"role": "System Manager",
|
|
||||||
"share": 1,
|
|
||||||
"write": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"create": 1,
|
|
||||||
"delete": 1,
|
|
||||||
"email": 1,
|
|
||||||
"export": 1,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
|
||||||
"report": 1,
|
|
||||||
"role": "Website Manager",
|
|
||||||
"share": 1,
|
|
||||||
"write": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"create": 1,
|
|
||||||
"delete": 1,
|
|
||||||
"email": 1,
|
|
||||||
"export": 1,
|
|
||||||
"print": 1,
|
|
||||||
"report": 1,
|
|
||||||
"role": "Customer",
|
|
||||||
"share": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"sort_field": "modified",
|
|
||||||
"sort_order": "DESC",
|
|
||||||
"track_changes": 1
|
|
||||||
}
|
|
@ -1,153 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
|
||||||
# For license information, please see license.txt
|
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
import frappe
|
|
||||||
from frappe import _
|
|
||||||
from frappe.contacts.doctype.contact.contact import get_contact_name
|
|
||||||
from frappe.model.document import Document
|
|
||||||
from frappe.utils import cint, flt
|
|
||||||
|
|
||||||
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
|
|
||||||
get_shopping_cart_settings,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class UnverifiedReviewer(frappe.ValidationError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ItemReview(Document):
|
|
||||||
def after_insert(self):
|
|
||||||
# regenerate cache on review creation
|
|
||||||
reviews_dict = get_queried_reviews(self.website_item)
|
|
||||||
set_reviews_in_cache(self.website_item, reviews_dict)
|
|
||||||
|
|
||||||
def after_delete(self):
|
|
||||||
# regenerate cache on review deletion
|
|
||||||
reviews_dict = get_queried_reviews(self.website_item)
|
|
||||||
set_reviews_in_cache(self.website_item, reviews_dict)
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def get_item_reviews(web_item, start=0, end=10, data=None):
|
|
||||||
"Get Website Item Review Data."
|
|
||||||
start, end = cint(start), cint(end)
|
|
||||||
settings = get_shopping_cart_settings()
|
|
||||||
|
|
||||||
# Get cached reviews for first page (start=0)
|
|
||||||
# avoid cache when page is different
|
|
||||||
from_cache = not bool(start)
|
|
||||||
|
|
||||||
if not data:
|
|
||||||
data = frappe._dict()
|
|
||||||
|
|
||||||
if settings and settings.get("enable_reviews"):
|
|
||||||
reviews_cache = frappe.cache().hget("item_reviews", web_item)
|
|
||||||
if from_cache and reviews_cache:
|
|
||||||
data = reviews_cache
|
|
||||||
else:
|
|
||||||
data = get_queried_reviews(web_item, start, end, data)
|
|
||||||
if from_cache:
|
|
||||||
set_reviews_in_cache(web_item, data)
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def get_queried_reviews(web_item, start=0, end=10, data=None):
|
|
||||||
"""
|
|
||||||
Query Website Item wise reviews and cache if needed.
|
|
||||||
Cache stores only first page of reviews i.e. 10 reviews maximum.
|
|
||||||
Returns:
|
|
||||||
dict: Containing reviews, average ratings, % of reviews per rating and total reviews.
|
|
||||||
"""
|
|
||||||
if not data:
|
|
||||||
data = frappe._dict()
|
|
||||||
|
|
||||||
data.reviews = frappe.db.get_all(
|
|
||||||
"Item Review",
|
|
||||||
filters={"website_item": web_item},
|
|
||||||
fields=["*"],
|
|
||||||
limit_start=start,
|
|
||||||
limit_page_length=end,
|
|
||||||
)
|
|
||||||
|
|
||||||
rating_data = frappe.db.get_all(
|
|
||||||
"Item Review",
|
|
||||||
filters={"website_item": web_item},
|
|
||||||
fields=["avg(rating) as average, count(*) as total"],
|
|
||||||
)[0]
|
|
||||||
|
|
||||||
data.average_rating = flt(rating_data.average, 1)
|
|
||||||
data.average_whole_rating = flt(data.average_rating, 0)
|
|
||||||
|
|
||||||
# get % of reviews per rating
|
|
||||||
reviews_per_rating = []
|
|
||||||
for i in range(1, 6):
|
|
||||||
count = frappe.db.get_all(
|
|
||||||
"Item Review", filters={"website_item": web_item, "rating": i}, fields=["count(*) as count"]
|
|
||||||
)[0].count
|
|
||||||
|
|
||||||
percent = flt((count / rating_data.total or 1) * 100, 0) if count else 0
|
|
||||||
reviews_per_rating.append(percent)
|
|
||||||
|
|
||||||
data.reviews_per_rating = reviews_per_rating
|
|
||||||
data.total_reviews = rating_data.total
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def set_reviews_in_cache(web_item, reviews_dict):
|
|
||||||
frappe.cache().hset("item_reviews", web_item, reviews_dict)
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def add_item_review(web_item, title, rating, comment=None):
|
|
||||||
"""Add an Item Review by a user if non-existent."""
|
|
||||||
if frappe.session.user == "Guest":
|
|
||||||
# guest user should not reach here ideally in the case they do via an API, throw error
|
|
||||||
frappe.throw(_("You are not verified to write a review yet."), exc=UnverifiedReviewer)
|
|
||||||
|
|
||||||
if not frappe.db.exists("Item Review", {"user": frappe.session.user, "website_item": web_item}):
|
|
||||||
doc = frappe.get_doc(
|
|
||||||
{
|
|
||||||
"doctype": "Item Review",
|
|
||||||
"user": frappe.session.user,
|
|
||||||
"customer": get_customer(),
|
|
||||||
"website_item": web_item,
|
|
||||||
"item": frappe.db.get_value("Website Item", web_item, "item_code"),
|
|
||||||
"review_title": title,
|
|
||||||
"rating": rating,
|
|
||||||
"comment": comment,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
doc.published_on = datetime.today().strftime("%d %B %Y")
|
|
||||||
doc.insert()
|
|
||||||
|
|
||||||
|
|
||||||
def get_customer(silent=False):
|
|
||||||
"""
|
|
||||||
silent: Return customer if exists else return nothing. Dont throw error.
|
|
||||||
"""
|
|
||||||
user = frappe.session.user
|
|
||||||
contact_name = get_contact_name(user)
|
|
||||||
customer = None
|
|
||||||
|
|
||||||
if contact_name:
|
|
||||||
contact = frappe.get_doc("Contact", contact_name)
|
|
||||||
for link in contact.links:
|
|
||||||
if link.link_doctype == "Customer":
|
|
||||||
customer = link.link_name
|
|
||||||
break
|
|
||||||
|
|
||||||
if customer:
|
|
||||||
return frappe.db.get_value("Customer", customer)
|
|
||||||
elif silent:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
# should not reach here unless via an API
|
|
||||||
frappe.throw(
|
|
||||||
_("You are not a verified customer yet. Please contact us to proceed."), exc=UnverifiedReviewer
|
|
||||||
)
|
|
@ -1,84 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
|
||||||
# See license.txt
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import frappe
|
|
||||||
from frappe.core.doctype.user_permission.test_user_permission import create_user
|
|
||||||
|
|
||||||
from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import (
|
|
||||||
setup_e_commerce_settings,
|
|
||||||
)
|
|
||||||
from erpnext.e_commerce.doctype.item_review.item_review import (
|
|
||||||
UnverifiedReviewer,
|
|
||||||
add_item_review,
|
|
||||||
get_item_reviews,
|
|
||||||
)
|
|
||||||
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
|
|
||||||
from erpnext.e_commerce.shopping_cart.cart import get_party
|
|
||||||
from erpnext.stock.doctype.item.test_item import make_item
|
|
||||||
|
|
||||||
|
|
||||||
class TestItemReview(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
item = make_item("Test Mobile Phone")
|
|
||||||
if not frappe.db.exists("Website Item", {"item_code": "Test Mobile Phone"}):
|
|
||||||
make_website_item(item, save=True)
|
|
||||||
|
|
||||||
setup_e_commerce_settings({"enable_reviews": 1})
|
|
||||||
frappe.local.shopping_cart_settings = None
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
frappe.get_cached_doc("Website Item", {"item_code": "Test Mobile Phone"}).delete()
|
|
||||||
setup_e_commerce_settings({"enable_reviews": 0})
|
|
||||||
|
|
||||||
def test_add_and_get_item_reviews_from_customer(self):
|
|
||||||
"Add / Get Reviews from a User that is a valid customer (has added to cart or purchased in the past)"
|
|
||||||
# create user
|
|
||||||
web_item = frappe.db.get_value("Website Item", {"item_code": "Test Mobile Phone"})
|
|
||||||
test_user = create_user("test_reviewer@example.com", "Customer")
|
|
||||||
frappe.set_user(test_user.name)
|
|
||||||
|
|
||||||
# create customer and contact against user
|
|
||||||
customer = get_party()
|
|
||||||
|
|
||||||
# post review on "Test Mobile Phone"
|
|
||||||
try:
|
|
||||||
add_item_review(web_item, "Great Product", 3, "Would recommend this product")
|
|
||||||
review_name = frappe.db.get_value("Item Review", {"website_item": web_item})
|
|
||||||
except Exception:
|
|
||||||
self.fail(f"Error while publishing review for {web_item}")
|
|
||||||
|
|
||||||
review_data = get_item_reviews(web_item, 0, 10)
|
|
||||||
|
|
||||||
self.assertEqual(len(review_data.reviews), 1)
|
|
||||||
self.assertEqual(review_data.average_rating, 3)
|
|
||||||
self.assertEqual(review_data.reviews_per_rating[2], 100)
|
|
||||||
|
|
||||||
# tear down
|
|
||||||
frappe.set_user("Administrator")
|
|
||||||
frappe.delete_doc("Item Review", review_name)
|
|
||||||
customer.delete()
|
|
||||||
|
|
||||||
def test_add_item_review_from_non_customer(self):
|
|
||||||
"Check if logged in user (who is not a customer yet) is blocked from posting reviews."
|
|
||||||
web_item = frappe.db.get_value("Website Item", {"item_code": "Test Mobile Phone"})
|
|
||||||
test_user = create_user("test_reviewer@example.com", "Customer")
|
|
||||||
frappe.set_user(test_user.name)
|
|
||||||
|
|
||||||
with self.assertRaises(UnverifiedReviewer):
|
|
||||||
add_item_review(web_item, "Great Product", 3, "Would recommend this product")
|
|
||||||
|
|
||||||
# tear down
|
|
||||||
frappe.set_user("Administrator")
|
|
||||||
|
|
||||||
def test_add_item_reviews_from_guest_user(self):
|
|
||||||
"Check if Guest user is blocked from posting reviews."
|
|
||||||
web_item = frappe.db.get_value("Website Item", {"item_code": "Test Mobile Phone"})
|
|
||||||
frappe.set_user("Guest")
|
|
||||||
|
|
||||||
with self.assertRaises(UnverifiedReviewer):
|
|
||||||
add_item_review(web_item, "Great Product", 3, "Would recommend this product")
|
|
||||||
|
|
||||||
# tear down
|
|
||||||
frappe.set_user("Administrator")
|
|
@ -1,88 +0,0 @@
|
|||||||
{
|
|
||||||
"actions": [],
|
|
||||||
"creation": "2021-07-12 20:52:12.503470",
|
|
||||||
"doctype": "DocType",
|
|
||||||
"editable_grid": 1,
|
|
||||||
"engine": "InnoDB",
|
|
||||||
"field_order": [
|
|
||||||
"website_item",
|
|
||||||
"website_item_name",
|
|
||||||
"column_break_2",
|
|
||||||
"item_code",
|
|
||||||
"more_information_section",
|
|
||||||
"route",
|
|
||||||
"column_break_6",
|
|
||||||
"website_item_image",
|
|
||||||
"website_item_thumbnail"
|
|
||||||
],
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldname": "website_item",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Website Item",
|
|
||||||
"options": "Website Item"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fetch_from": "website_item.web_item_name",
|
|
||||||
"fieldname": "website_item_name",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Website Item Name",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_2",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "more_information_section",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "More Information"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fetch_from": "website_item.route",
|
|
||||||
"fieldname": "route",
|
|
||||||
"fieldtype": "Small Text",
|
|
||||||
"label": "Route",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fetch_from": "website_item.website_image",
|
|
||||||
"fieldname": "website_item_image",
|
|
||||||
"fieldtype": "Attach",
|
|
||||||
"label": "Website Item Image",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_6",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fetch_from": "website_item.thumbnail",
|
|
||||||
"fieldname": "website_item_thumbnail",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "Website Item Thumbnail",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fetch_from": "website_item.item_code",
|
|
||||||
"fieldname": "item_code",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "Item Code"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"index_web_pages_for_search": 1,
|
|
||||||
"istable": 1,
|
|
||||||
"links": [],
|
|
||||||
"modified": "2022-06-28 16:44:24.718728",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"module": "E-commerce",
|
|
||||||
"name": "Recommended Items",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"permissions": [],
|
|
||||||
"sort_field": "modified",
|
|
||||||
"sort_order": "DESC",
|
|
||||||
"states": [],
|
|
||||||
"track_changes": 1
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
|
||||||
# For license information, please see license.txt
|
|
||||||
|
|
||||||
# import frappe
|
|
||||||
from frappe.model.document import Document
|
|
||||||
|
|
||||||
|
|
||||||
class RecommendedItems(Document):
|
|
||||||
pass
|
|
@ -1,7 +0,0 @@
|
|||||||
{% extends "templates/web.html" %}
|
|
||||||
|
|
||||||
{% block page_content %}
|
|
||||||
<h1>{{ title }}</h1>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
<!-- this is a sample default web page template -->
|
|
@ -1,4 +0,0 @@
|
|||||||
<div>
|
|
||||||
<a href="{{ doc.route }}">{{ doc.title or doc.name }}</a>
|
|
||||||
</div>
|
|
||||||
<!-- this is a sample default list template -->
|
|
@ -1,564 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
|
||||||
# See license.txt
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import frappe
|
|
||||||
|
|
||||||
from erpnext.controllers.item_variant import create_variant
|
|
||||||
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
|
|
||||||
get_shopping_cart_settings,
|
|
||||||
)
|
|
||||||
from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import (
|
|
||||||
setup_e_commerce_settings,
|
|
||||||
)
|
|
||||||
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
|
|
||||||
from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website
|
|
||||||
from erpnext.stock.doctype.item.item import DataValidationError
|
|
||||||
from erpnext.stock.doctype.item.test_item import make_item
|
|
||||||
|
|
||||||
WEBITEM_DESK_TESTS = ("test_website_item_desk_item_sync", "test_publish_variant_and_template")
|
|
||||||
WEBITEM_PRICE_TESTS = (
|
|
||||||
"test_website_item_price_for_logged_in_user",
|
|
||||||
"test_website_item_price_for_guest_user",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestWebsiteItem(unittest.TestCase):
|
|
||||||
@classmethod
|
|
||||||
def setUpClass(cls):
|
|
||||||
setup_e_commerce_settings(
|
|
||||||
{
|
|
||||||
"company": "_Test Company",
|
|
||||||
"enabled": 1,
|
|
||||||
"default_customer_group": "_Test Customer Group",
|
|
||||||
"price_list": "_Test Price List India",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def tearDownClass(cls):
|
|
||||||
frappe.db.rollback()
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
if self._testMethodName in WEBITEM_DESK_TESTS:
|
|
||||||
make_item(
|
|
||||||
"Test Web Item",
|
|
||||||
{
|
|
||||||
"has_variant": 1,
|
|
||||||
"variant_based_on": "Item Attribute",
|
|
||||||
"attributes": [{"attribute": "Test Size"}],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
elif self._testMethodName in WEBITEM_PRICE_TESTS:
|
|
||||||
create_user_and_customer_if_not_exists(
|
|
||||||
"test_contact_customer@example.com", "_Test Contact For _Test Customer"
|
|
||||||
)
|
|
||||||
create_regular_web_item()
|
|
||||||
make_web_item_price(item_code="Test Mobile Phone")
|
|
||||||
|
|
||||||
# Note: When testing web item pricing rule logged-in user pricing rule must differ from guest pricing rule or test will falsely pass.
|
|
||||||
# This is because make_web_pricing_rule creates a pricing rule "selling": 1, without specifying "applicable_for". Therefor,
|
|
||||||
# when testing for logged-in user the test will get the previous pricing rule because "selling" is still true.
|
|
||||||
#
|
|
||||||
# I've attempted to mitigate this by setting applicable_for=Customer, and customer=Guest however, this only results in PermissionError failing the test.
|
|
||||||
make_web_pricing_rule(
|
|
||||||
title="Test Pricing Rule for Test Mobile Phone", item_code="Test Mobile Phone", selling=1
|
|
||||||
)
|
|
||||||
make_web_pricing_rule(
|
|
||||||
title="Test Pricing Rule for Test Mobile Phone (Customer)",
|
|
||||||
item_code="Test Mobile Phone",
|
|
||||||
selling=1,
|
|
||||||
discount_percentage="25",
|
|
||||||
applicable_for="Customer",
|
|
||||||
customer="_Test Customer",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_index_creation(self):
|
|
||||||
"Check if index is getting created in db."
|
|
||||||
from erpnext.e_commerce.doctype.website_item.website_item import on_doctype_update
|
|
||||||
|
|
||||||
on_doctype_update()
|
|
||||||
|
|
||||||
indices = frappe.db.sql("show index from `tabWebsite Item`", as_dict=1)
|
|
||||||
expected_columns = {"route", "item_group", "brand"}
|
|
||||||
for index in indices:
|
|
||||||
expected_columns.discard(index.get("Column_name"))
|
|
||||||
|
|
||||||
if expected_columns:
|
|
||||||
self.fail(f"Expected db index on these columns: {', '.join(expected_columns)}")
|
|
||||||
|
|
||||||
def test_website_item_desk_item_sync(self):
|
|
||||||
"Check creation/updation/deletion of Website Item and its impact on Item master."
|
|
||||||
web_item = None
|
|
||||||
item = make_item("Test Web Item") # will return item if exists
|
|
||||||
try:
|
|
||||||
web_item = make_website_item(item, save=False)
|
|
||||||
web_item.save()
|
|
||||||
except Exception:
|
|
||||||
self.fail(f"Error while creating website item for {item}")
|
|
||||||
|
|
||||||
# check if website item was created
|
|
||||||
self.assertTrue(bool(web_item))
|
|
||||||
self.assertTrue(bool(web_item.route))
|
|
||||||
|
|
||||||
item.reload()
|
|
||||||
self.assertEqual(web_item.published, 1)
|
|
||||||
self.assertEqual(item.published_in_website, 1) # check if item was back updated
|
|
||||||
self.assertEqual(web_item.item_group, item.item_group)
|
|
||||||
|
|
||||||
# check if changing item data changes it in website item
|
|
||||||
item.item_name = "Test Web Item 1"
|
|
||||||
item.stock_uom = "Unit"
|
|
||||||
item.save()
|
|
||||||
web_item.reload()
|
|
||||||
self.assertEqual(web_item.item_name, item.item_name)
|
|
||||||
self.assertEqual(web_item.stock_uom, item.stock_uom)
|
|
||||||
|
|
||||||
# check if disabling item unpublished website item
|
|
||||||
item.disabled = 1
|
|
||||||
item.save()
|
|
||||||
web_item.reload()
|
|
||||||
self.assertEqual(web_item.published, 0)
|
|
||||||
|
|
||||||
# check if website item deletion, unpublishes desk item
|
|
||||||
web_item.delete()
|
|
||||||
item.reload()
|
|
||||||
self.assertEqual(item.published_in_website, 0)
|
|
||||||
|
|
||||||
item.delete()
|
|
||||||
|
|
||||||
def test_publish_variant_and_template(self):
|
|
||||||
"Check if template is published on publishing variant."
|
|
||||||
# template "Test Web Item" created on setUp
|
|
||||||
variant = create_variant("Test Web Item", {"Test Size": "Large"})
|
|
||||||
variant.save()
|
|
||||||
|
|
||||||
# check if template is not published
|
|
||||||
self.assertIsNone(frappe.db.exists("Website Item", {"item_code": variant.variant_of}))
|
|
||||||
|
|
||||||
variant_web_item = make_website_item(variant, save=False)
|
|
||||||
variant_web_item.save()
|
|
||||||
|
|
||||||
# check if template is published
|
|
||||||
try:
|
|
||||||
template_web_item = frappe.get_doc("Website Item", {"item_code": variant.variant_of})
|
|
||||||
except frappe.DoesNotExistError:
|
|
||||||
self.fail(f"Template of {variant.item_code}, {variant.variant_of} not published")
|
|
||||||
|
|
||||||
# teardown
|
|
||||||
variant_web_item.delete()
|
|
||||||
template_web_item.delete()
|
|
||||||
variant.delete()
|
|
||||||
|
|
||||||
def test_impact_on_merging_items(self):
|
|
||||||
"Check if merging items is blocked if old and new items both have website items"
|
|
||||||
first_item = make_item("Test First Item")
|
|
||||||
second_item = make_item("Test Second Item")
|
|
||||||
|
|
||||||
first_web_item = make_website_item(first_item, save=False)
|
|
||||||
first_web_item.save()
|
|
||||||
second_web_item = make_website_item(second_item, save=False)
|
|
||||||
second_web_item.save()
|
|
||||||
|
|
||||||
with self.assertRaises(DataValidationError):
|
|
||||||
frappe.rename_doc("Item", "Test First Item", "Test Second Item", merge=True)
|
|
||||||
|
|
||||||
# tear down
|
|
||||||
second_web_item.delete()
|
|
||||||
first_web_item.delete()
|
|
||||||
second_item.delete()
|
|
||||||
first_item.delete()
|
|
||||||
|
|
||||||
# Website Item Portal Tests Begin
|
|
||||||
|
|
||||||
def test_website_item_breadcrumbs(self):
|
|
||||||
"""
|
|
||||||
Check if breadcrumbs include homepage, product listing navigation page,
|
|
||||||
parent item group(s) and item group
|
|
||||||
"""
|
|
||||||
from erpnext.setup.doctype.item_group.item_group import get_parent_item_groups
|
|
||||||
|
|
||||||
item_code = "Test Breadcrumb Item"
|
|
||||||
item = make_item(
|
|
||||||
item_code,
|
|
||||||
{
|
|
||||||
"item_group": "_Test Item Group B - 1",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if not frappe.db.exists("Website Item", {"item_code": item_code}):
|
|
||||||
web_item = make_website_item(item, save=False)
|
|
||||||
web_item.save()
|
|
||||||
else:
|
|
||||||
web_item = frappe.get_cached_doc("Website Item", {"item_code": item_code})
|
|
||||||
|
|
||||||
frappe.db.set_value("Item Group", "_Test Item Group B - 1", "show_in_website", 1)
|
|
||||||
frappe.db.set_value("Item Group", "_Test Item Group B", "show_in_website", 1)
|
|
||||||
|
|
||||||
breadcrumbs = get_parent_item_groups(item.item_group)
|
|
||||||
|
|
||||||
settings = frappe.get_cached_doc("E Commerce Settings")
|
|
||||||
if settings.enable_field_filters:
|
|
||||||
base_breadcrumb = "Shop by Category"
|
|
||||||
else:
|
|
||||||
base_breadcrumb = "All Products"
|
|
||||||
|
|
||||||
self.assertEqual(breadcrumbs[0]["name"], "Home")
|
|
||||||
self.assertEqual(breadcrumbs[1]["name"], base_breadcrumb)
|
|
||||||
self.assertEqual(breadcrumbs[2]["name"], "_Test Item Group B") # parent item group
|
|
||||||
self.assertEqual(breadcrumbs[3]["name"], "_Test Item Group B - 1")
|
|
||||||
|
|
||||||
# tear down
|
|
||||||
web_item.delete()
|
|
||||||
item.delete()
|
|
||||||
|
|
||||||
def test_website_item_price_for_logged_in_user(self):
|
|
||||||
"Check if price details are fetched correctly while logged in."
|
|
||||||
item_code = "Test Mobile Phone"
|
|
||||||
|
|
||||||
# show price in e commerce settings
|
|
||||||
setup_e_commerce_settings({"show_price": 1})
|
|
||||||
|
|
||||||
# price and pricing rule added via setUp
|
|
||||||
|
|
||||||
# login as customer with pricing rule
|
|
||||||
frappe.set_user("test_contact_customer@example.com")
|
|
||||||
|
|
||||||
# check if price and slashed price is fetched correctly
|
|
||||||
frappe.local.shopping_cart_settings = None
|
|
||||||
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
|
|
||||||
self.assertTrue(bool(data.product_info["price"]))
|
|
||||||
|
|
||||||
price_object = data.product_info["price"]
|
|
||||||
self.assertEqual(price_object.get("discount_percent"), 25.0)
|
|
||||||
self.assertEqual(price_object.get("price_list_rate"), 750)
|
|
||||||
self.assertEqual(price_object.get("formatted_mrp"), "₹ 1,000.00")
|
|
||||||
self.assertEqual(price_object.get("formatted_price"), "₹ 750.00")
|
|
||||||
self.assertEqual(price_object.get("formatted_discount_percent"), "25.0%")
|
|
||||||
|
|
||||||
# switch to admin and disable show price
|
|
||||||
frappe.set_user("Administrator")
|
|
||||||
setup_e_commerce_settings({"show_price": 0})
|
|
||||||
|
|
||||||
# price should not be fetched for logged in user.
|
|
||||||
frappe.set_user("test_contact_customer@example.com")
|
|
||||||
frappe.local.shopping_cart_settings = None
|
|
||||||
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
|
|
||||||
self.assertFalse(bool(data.product_info["price"]))
|
|
||||||
|
|
||||||
# tear down
|
|
||||||
frappe.set_user("Administrator")
|
|
||||||
|
|
||||||
def test_website_item_price_for_guest_user(self):
|
|
||||||
"Check if price details are fetched correctly for guest user."
|
|
||||||
item_code = "Test Mobile Phone"
|
|
||||||
|
|
||||||
# show price for guest user in e commerce settings
|
|
||||||
setup_e_commerce_settings({"show_price": 1, "hide_price_for_guest": 0})
|
|
||||||
|
|
||||||
# price and pricing rule added via setUp
|
|
||||||
|
|
||||||
# switch to guest user
|
|
||||||
frappe.set_user("Guest")
|
|
||||||
|
|
||||||
# price should be fetched
|
|
||||||
frappe.local.shopping_cart_settings = None
|
|
||||||
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
|
|
||||||
self.assertTrue(bool(data.product_info["price"]))
|
|
||||||
|
|
||||||
price_object = data.product_info["price"]
|
|
||||||
self.assertEqual(price_object.get("discount_percent"), 10)
|
|
||||||
self.assertEqual(price_object.get("price_list_rate"), 900)
|
|
||||||
|
|
||||||
# hide price for guest user
|
|
||||||
frappe.set_user("Administrator")
|
|
||||||
setup_e_commerce_settings({"hide_price_for_guest": 1})
|
|
||||||
frappe.set_user("Guest")
|
|
||||||
|
|
||||||
# price should not be fetched
|
|
||||||
frappe.local.shopping_cart_settings = None
|
|
||||||
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
|
|
||||||
self.assertFalse(bool(data.product_info["price"]))
|
|
||||||
|
|
||||||
# tear down
|
|
||||||
frappe.set_user("Administrator")
|
|
||||||
|
|
||||||
def test_website_item_stock_when_out_of_stock(self):
|
|
||||||
"""
|
|
||||||
Check if stock details are fetched correctly for empty inventory when:
|
|
||||||
1) Showing stock availability enabled:
|
|
||||||
- Warehouse unset
|
|
||||||
- Warehouse set
|
|
||||||
2) Showing stock availability disabled
|
|
||||||
"""
|
|
||||||
item_code = "Test Mobile Phone"
|
|
||||||
create_regular_web_item()
|
|
||||||
setup_e_commerce_settings({"show_stock_availability": 1})
|
|
||||||
|
|
||||||
frappe.local.shopping_cart_settings = None
|
|
||||||
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
|
|
||||||
|
|
||||||
# check if stock details are fetched and item not in stock without warehouse set
|
|
||||||
self.assertFalse(bool(data.product_info["in_stock"]))
|
|
||||||
self.assertFalse(bool(data.product_info["stock_qty"]))
|
|
||||||
|
|
||||||
# set warehouse
|
|
||||||
frappe.db.set_value(
|
|
||||||
"Website Item", {"item_code": item_code}, "website_warehouse", "_Test Warehouse - _TC"
|
|
||||||
)
|
|
||||||
|
|
||||||
# check if stock details are fetched and item not in stock with warehouse set
|
|
||||||
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
|
|
||||||
self.assertFalse(bool(data.product_info["in_stock"]))
|
|
||||||
self.assertEqual(data.product_info["stock_qty"], 0)
|
|
||||||
|
|
||||||
# disable show stock availability
|
|
||||||
setup_e_commerce_settings({"show_stock_availability": 0})
|
|
||||||
frappe.local.shopping_cart_settings = None
|
|
||||||
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
|
|
||||||
|
|
||||||
# check if stock detail attributes are not fetched if stock availability is hidden
|
|
||||||
self.assertIsNone(data.product_info.get("in_stock"))
|
|
||||||
self.assertIsNone(data.product_info.get("stock_qty"))
|
|
||||||
self.assertIsNone(data.product_info.get("show_stock_qty"))
|
|
||||||
|
|
||||||
# tear down
|
|
||||||
frappe.get_cached_doc("Website Item", {"item_code": "Test Mobile Phone"}).delete()
|
|
||||||
|
|
||||||
def test_website_item_stock_when_in_stock(self):
|
|
||||||
"""
|
|
||||||
Check if stock details are fetched correctly for available inventory when:
|
|
||||||
1) Showing stock availability enabled:
|
|
||||||
- Warehouse set
|
|
||||||
- Warehouse unset
|
|
||||||
2) Showing stock availability disabled
|
|
||||||
"""
|
|
||||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
|
||||||
|
|
||||||
item_code = "Test Mobile Phone"
|
|
||||||
create_regular_web_item()
|
|
||||||
setup_e_commerce_settings({"show_stock_availability": 1})
|
|
||||||
frappe.local.shopping_cart_settings = None
|
|
||||||
|
|
||||||
# set warehouse
|
|
||||||
frappe.db.set_value(
|
|
||||||
"Website Item", {"item_code": item_code}, "website_warehouse", "_Test Warehouse - _TC"
|
|
||||||
)
|
|
||||||
|
|
||||||
# stock up item
|
|
||||||
stock_entry = make_stock_entry(
|
|
||||||
item_code=item_code, target="_Test Warehouse - _TC", qty=2, rate=100
|
|
||||||
)
|
|
||||||
|
|
||||||
# check if stock details are fetched and item is in stock with warehouse set
|
|
||||||
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
|
|
||||||
self.assertTrue(bool(data.product_info["in_stock"]))
|
|
||||||
self.assertEqual(data.product_info["stock_qty"], 2)
|
|
||||||
|
|
||||||
# unset warehouse
|
|
||||||
frappe.db.set_value("Website Item", {"item_code": item_code}, "website_warehouse", "")
|
|
||||||
|
|
||||||
# check if stock details are fetched and item not in stock without warehouse set
|
|
||||||
# (even though it has stock in some warehouse)
|
|
||||||
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
|
|
||||||
self.assertFalse(bool(data.product_info["in_stock"]))
|
|
||||||
self.assertFalse(data.product_info["stock_qty"])
|
|
||||||
|
|
||||||
# disable show stock availability
|
|
||||||
setup_e_commerce_settings({"show_stock_availability": 0})
|
|
||||||
frappe.local.shopping_cart_settings = None
|
|
||||||
data = get_product_info_for_website(item_code, skip_quotation_creation=True)
|
|
||||||
|
|
||||||
# check if stock detail attributes are not fetched if stock availability is hidden
|
|
||||||
self.assertIsNone(data.product_info.get("in_stock"))
|
|
||||||
self.assertIsNone(data.product_info.get("stock_qty"))
|
|
||||||
self.assertIsNone(data.product_info.get("show_stock_qty"))
|
|
||||||
|
|
||||||
# tear down
|
|
||||||
stock_entry.cancel()
|
|
||||||
frappe.get_cached_doc("Website Item", {"item_code": "Test Mobile Phone"}).delete()
|
|
||||||
|
|
||||||
def test_recommended_item(self):
|
|
||||||
"Check if added recommended items are fetched correctly."
|
|
||||||
item_code = "Test Mobile Phone"
|
|
||||||
web_item = create_regular_web_item(item_code)
|
|
||||||
|
|
||||||
setup_e_commerce_settings({"enable_recommendations": 1, "show_price": 1})
|
|
||||||
|
|
||||||
# create recommended web item and price for it
|
|
||||||
recommended_web_item = create_regular_web_item("Test Mobile Phone 1")
|
|
||||||
make_web_item_price(item_code="Test Mobile Phone 1")
|
|
||||||
|
|
||||||
# add recommended item to first web item
|
|
||||||
web_item.append("recommended_items", {"website_item": recommended_web_item.name})
|
|
||||||
web_item.save()
|
|
||||||
|
|
||||||
frappe.local.shopping_cart_settings = None
|
|
||||||
e_commerce_settings = get_shopping_cart_settings()
|
|
||||||
recommended_items = web_item.get_recommended_items(e_commerce_settings)
|
|
||||||
|
|
||||||
# test results if show price is enabled
|
|
||||||
self.assertEqual(len(recommended_items), 1)
|
|
||||||
recomm_item = recommended_items[0]
|
|
||||||
self.assertEqual(recomm_item.get("website_item_name"), "Test Mobile Phone 1")
|
|
||||||
self.assertTrue(bool(recomm_item.get("price_info"))) # price fetched
|
|
||||||
|
|
||||||
price_info = recomm_item.get("price_info")
|
|
||||||
self.assertEqual(price_info.get("price_list_rate"), 1000)
|
|
||||||
self.assertEqual(price_info.get("formatted_price"), "₹ 1,000.00")
|
|
||||||
|
|
||||||
# test results if show price is disabled
|
|
||||||
setup_e_commerce_settings({"show_price": 0})
|
|
||||||
|
|
||||||
frappe.local.shopping_cart_settings = None
|
|
||||||
e_commerce_settings = get_shopping_cart_settings()
|
|
||||||
recommended_items = web_item.get_recommended_items(e_commerce_settings)
|
|
||||||
|
|
||||||
self.assertEqual(len(recommended_items), 1)
|
|
||||||
self.assertFalse(bool(recommended_items[0].get("price_info"))) # price not fetched
|
|
||||||
|
|
||||||
# tear down
|
|
||||||
web_item.delete()
|
|
||||||
recommended_web_item.delete()
|
|
||||||
frappe.get_cached_doc("Item", "Test Mobile Phone 1").delete()
|
|
||||||
|
|
||||||
def test_recommended_item_for_guest_user(self):
|
|
||||||
"Check if added recommended items are fetched correctly for guest user."
|
|
||||||
item_code = "Test Mobile Phone"
|
|
||||||
web_item = create_regular_web_item(item_code)
|
|
||||||
|
|
||||||
# price visible to guests
|
|
||||||
setup_e_commerce_settings(
|
|
||||||
{"enable_recommendations": 1, "show_price": 1, "hide_price_for_guest": 0}
|
|
||||||
)
|
|
||||||
|
|
||||||
# create recommended web item and price for it
|
|
||||||
recommended_web_item = create_regular_web_item("Test Mobile Phone 1")
|
|
||||||
make_web_item_price(item_code="Test Mobile Phone 1")
|
|
||||||
|
|
||||||
# add recommended item to first web item
|
|
||||||
web_item.append("recommended_items", {"website_item": recommended_web_item.name})
|
|
||||||
web_item.save()
|
|
||||||
|
|
||||||
frappe.set_user("Guest")
|
|
||||||
|
|
||||||
frappe.local.shopping_cart_settings = None
|
|
||||||
e_commerce_settings = get_shopping_cart_settings()
|
|
||||||
recommended_items = web_item.get_recommended_items(e_commerce_settings)
|
|
||||||
|
|
||||||
# test results if show price is enabled
|
|
||||||
self.assertEqual(len(recommended_items), 1)
|
|
||||||
self.assertTrue(bool(recommended_items[0].get("price_info"))) # price fetched
|
|
||||||
|
|
||||||
# price hidden from guests
|
|
||||||
frappe.set_user("Administrator")
|
|
||||||
setup_e_commerce_settings({"hide_price_for_guest": 1})
|
|
||||||
frappe.set_user("Guest")
|
|
||||||
|
|
||||||
frappe.local.shopping_cart_settings = None
|
|
||||||
e_commerce_settings = get_shopping_cart_settings()
|
|
||||||
recommended_items = web_item.get_recommended_items(e_commerce_settings)
|
|
||||||
|
|
||||||
# test results if show price is enabled
|
|
||||||
self.assertEqual(len(recommended_items), 1)
|
|
||||||
self.assertFalse(bool(recommended_items[0].get("price_info"))) # price fetched
|
|
||||||
|
|
||||||
# tear down
|
|
||||||
frappe.set_user("Administrator")
|
|
||||||
web_item.delete()
|
|
||||||
recommended_web_item.delete()
|
|
||||||
frappe.get_cached_doc("Item", "Test Mobile Phone 1").delete()
|
|
||||||
|
|
||||||
|
|
||||||
def create_regular_web_item(item_code=None, item_args=None, web_args=None):
|
|
||||||
"Create Regular Item and Website Item."
|
|
||||||
item_code = item_code or "Test Mobile Phone"
|
|
||||||
item = make_item(item_code, properties=item_args)
|
|
||||||
|
|
||||||
if not frappe.db.exists("Website Item", {"item_code": item_code}):
|
|
||||||
web_item = make_website_item(item, save=False)
|
|
||||||
if web_args:
|
|
||||||
web_item.update(web_args)
|
|
||||||
web_item.save()
|
|
||||||
else:
|
|
||||||
web_item = frappe.get_cached_doc("Website Item", {"item_code": item_code})
|
|
||||||
|
|
||||||
return web_item
|
|
||||||
|
|
||||||
|
|
||||||
def make_web_item_price(**kwargs):
|
|
||||||
item_code = kwargs.get("item_code")
|
|
||||||
if not item_code:
|
|
||||||
return
|
|
||||||
|
|
||||||
if not frappe.db.exists("Item Price", {"item_code": item_code}):
|
|
||||||
item_price = frappe.get_doc(
|
|
||||||
{
|
|
||||||
"doctype": "Item Price",
|
|
||||||
"item_code": item_code,
|
|
||||||
"price_list": kwargs.get("price_list") or "_Test Price List India",
|
|
||||||
"price_list_rate": kwargs.get("price_list_rate") or 1000,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
item_price.insert()
|
|
||||||
else:
|
|
||||||
item_price = frappe.get_cached_doc("Item Price", {"item_code": item_code})
|
|
||||||
|
|
||||||
return item_price
|
|
||||||
|
|
||||||
|
|
||||||
def make_web_pricing_rule(**kwargs):
|
|
||||||
title = kwargs.get("title")
|
|
||||||
if not title:
|
|
||||||
return
|
|
||||||
|
|
||||||
if not frappe.db.exists("Pricing Rule", title):
|
|
||||||
pricing_rule = frappe.get_doc(
|
|
||||||
{
|
|
||||||
"doctype": "Pricing Rule",
|
|
||||||
"title": title,
|
|
||||||
"apply_on": kwargs.get("apply_on") or "Item Code",
|
|
||||||
"items": [{"item_code": kwargs.get("item_code")}],
|
|
||||||
"selling": kwargs.get("selling") or 0,
|
|
||||||
"buying": kwargs.get("buying") or 0,
|
|
||||||
"rate_or_discount": kwargs.get("rate_or_discount") or "Discount Percentage",
|
|
||||||
"discount_percentage": kwargs.get("discount_percentage") or 10,
|
|
||||||
"company": kwargs.get("company") or "_Test Company",
|
|
||||||
"currency": kwargs.get("currency") or "INR",
|
|
||||||
"for_price_list": kwargs.get("price_list") or "_Test Price List India",
|
|
||||||
"applicable_for": kwargs.get("applicable_for") or "",
|
|
||||||
"customer": kwargs.get("customer") or "",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
pricing_rule.insert()
|
|
||||||
else:
|
|
||||||
pricing_rule = frappe.get_doc("Pricing Rule", {"title": title})
|
|
||||||
|
|
||||||
return pricing_rule
|
|
||||||
|
|
||||||
|
|
||||||
def create_user_and_customer_if_not_exists(email, first_name=None):
|
|
||||||
if frappe.db.exists("User", email):
|
|
||||||
return
|
|
||||||
|
|
||||||
frappe.get_doc(
|
|
||||||
{
|
|
||||||
"doctype": "User",
|
|
||||||
"user_type": "Website User",
|
|
||||||
"email": email,
|
|
||||||
"send_welcome_email": 0,
|
|
||||||
"first_name": first_name or email.split("@")[0],
|
|
||||||
}
|
|
||||||
).insert(ignore_permissions=True)
|
|
||||||
|
|
||||||
contact = frappe.get_last_doc("Contact", filters={"email_id": email})
|
|
||||||
link = contact.append("links", {})
|
|
||||||
link.link_doctype = "Customer"
|
|
||||||
link.link_name = "_Test Customer"
|
|
||||||
link.link_title = "_Test Customer"
|
|
||||||
contact.save()
|
|
||||||
|
|
||||||
|
|
||||||
test_dependencies = ["Price List", "Item Price", "Customer", "Contact", "Item"]
|
|
@ -1,37 +0,0 @@
|
|||||||
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
|
||||||
// For license information, please see license.txt
|
|
||||||
|
|
||||||
frappe.ui.form.on('Website Item', {
|
|
||||||
onload: (frm) => {
|
|
||||||
// should never check Private
|
|
||||||
frm.fields_dict["website_image"].df.is_private = 0;
|
|
||||||
},
|
|
||||||
|
|
||||||
refresh: (frm) => {
|
|
||||||
frm.add_custom_button(__("Prices"), function() {
|
|
||||||
frappe.set_route("List", "Item Price", {"item_code": frm.doc.item_code});
|
|
||||||
}, __("View"));
|
|
||||||
|
|
||||||
frm.add_custom_button(__("Stock"), function() {
|
|
||||||
frappe.route_options = {
|
|
||||||
"item_code": frm.doc.item_code
|
|
||||||
};
|
|
||||||
frappe.set_route("query-report", "Stock Balance");
|
|
||||||
}, __("View"));
|
|
||||||
|
|
||||||
frm.add_custom_button(__("E Commerce Settings"), function() {
|
|
||||||
frappe.set_route("Form", "E Commerce Settings");
|
|
||||||
}, __("View"));
|
|
||||||
},
|
|
||||||
|
|
||||||
copy_from_item_group: (frm) => {
|
|
||||||
return frm.call({
|
|
||||||
doc: frm.doc,
|
|
||||||
method: "copy_specification_from_item_group"
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
set_meta_tags: (frm) => {
|
|
||||||
frappe.utils.set_meta_tag(frm.doc.route);
|
|
||||||
}
|
|
||||||
});
|
|
@ -1,414 +0,0 @@
|
|||||||
{
|
|
||||||
"actions": [],
|
|
||||||
"allow_guest_to_view": 1,
|
|
||||||
"allow_import": 1,
|
|
||||||
"autoname": "naming_series",
|
|
||||||
"creation": "2021-02-09 21:06:14.441698",
|
|
||||||
"doctype": "DocType",
|
|
||||||
"editable_grid": 1,
|
|
||||||
"engine": "InnoDB",
|
|
||||||
"field_order": [
|
|
||||||
"naming_series",
|
|
||||||
"web_item_name",
|
|
||||||
"route",
|
|
||||||
"has_variants",
|
|
||||||
"variant_of",
|
|
||||||
"published",
|
|
||||||
"column_break_3",
|
|
||||||
"item_code",
|
|
||||||
"item_name",
|
|
||||||
"item_group",
|
|
||||||
"stock_uom",
|
|
||||||
"column_break_11",
|
|
||||||
"description",
|
|
||||||
"brand",
|
|
||||||
"display_section",
|
|
||||||
"website_image",
|
|
||||||
"website_image_alt",
|
|
||||||
"column_break_13",
|
|
||||||
"slideshow",
|
|
||||||
"thumbnail",
|
|
||||||
"stock_information_section",
|
|
||||||
"website_warehouse",
|
|
||||||
"column_break_24",
|
|
||||||
"on_backorder",
|
|
||||||
"section_break_17",
|
|
||||||
"short_description",
|
|
||||||
"web_long_description",
|
|
||||||
"column_break_27",
|
|
||||||
"website_specifications",
|
|
||||||
"copy_from_item_group",
|
|
||||||
"display_additional_information_section",
|
|
||||||
"show_tabbed_section",
|
|
||||||
"tabs",
|
|
||||||
"recommended_items_section",
|
|
||||||
"recommended_items",
|
|
||||||
"offers_section",
|
|
||||||
"offers",
|
|
||||||
"section_break_6",
|
|
||||||
"ranking",
|
|
||||||
"set_meta_tags",
|
|
||||||
"column_break_22",
|
|
||||||
"website_item_groups",
|
|
||||||
"advanced_display_section",
|
|
||||||
"website_content"
|
|
||||||
],
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"description": "Website display name",
|
|
||||||
"fetch_from": "item_code.item_name",
|
|
||||||
"fetch_if_empty": 1,
|
|
||||||
"fieldname": "web_item_name",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Website Item Name",
|
|
||||||
"reqd": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_3",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "item_code",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"label": "Item Code",
|
|
||||||
"options": "Item",
|
|
||||||
"read_only_depends_on": "eval:!doc.__islocal",
|
|
||||||
"reqd": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fetch_from": "item_code.item_name",
|
|
||||||
"fieldname": "item_name",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "Item Name",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"collapsible": 1,
|
|
||||||
"fieldname": "section_break_6",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "Search and SEO"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "route",
|
|
||||||
"fieldtype": "Small Text",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Route",
|
|
||||||
"no_copy": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Items with higher ranking will be shown higher",
|
|
||||||
"fieldname": "ranking",
|
|
||||||
"fieldtype": "Int",
|
|
||||||
"label": "Ranking"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Show a slideshow at the top of the page",
|
|
||||||
"fieldname": "slideshow",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"label": "Slideshow",
|
|
||||||
"options": "Website Slideshow"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Item Image (if not slideshow)",
|
|
||||||
"fieldname": "website_image",
|
|
||||||
"fieldtype": "Attach Image",
|
|
||||||
"hidden": 1,
|
|
||||||
"in_preview": 1,
|
|
||||||
"label": "Website Image",
|
|
||||||
"print_hide": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Image Alternative Text",
|
|
||||||
"fieldname": "website_image_alt",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "Image Description"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "thumbnail",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "Thumbnail",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_13",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Show Stock availability based on this warehouse. If the parent warehouse is selected, then the system will display the consolidated available quantity of all child warehouses.",
|
|
||||||
"fieldname": "website_warehouse",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"ignore_user_permissions": 1,
|
|
||||||
"label": "Website Warehouse",
|
|
||||||
"options": "Warehouse"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "List this Item in multiple groups on the website.",
|
|
||||||
"fieldname": "website_item_groups",
|
|
||||||
"fieldtype": "Table",
|
|
||||||
"label": "Website Item Groups",
|
|
||||||
"options": "Website Item Group"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "set_meta_tags",
|
|
||||||
"fieldtype": "Button",
|
|
||||||
"label": "Set Meta Tags"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "section_break_17",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "Display Information"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "copy_from_item_group",
|
|
||||||
"fieldtype": "Button",
|
|
||||||
"label": "Copy From Item Group"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "website_specifications",
|
|
||||||
"fieldtype": "Table",
|
|
||||||
"label": "Website Specifications",
|
|
||||||
"options": "Item Website Specification"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "web_long_description",
|
|
||||||
"fieldtype": "Text Editor",
|
|
||||||
"label": "Website Description"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "You can use any valid Bootstrap 4 markup in this field. It will be shown on your Item Page.",
|
|
||||||
"fieldname": "website_content",
|
|
||||||
"fieldtype": "HTML Editor",
|
|
||||||
"label": "Website Content"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fetch_from": "item_code.item_group",
|
|
||||||
"fieldname": "item_group",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Item Group",
|
|
||||||
"options": "Item Group",
|
|
||||||
"read_only": 1,
|
|
||||||
"search_index": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "1",
|
|
||||||
"fieldname": "published",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Published"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"depends_on": "has_variants",
|
|
||||||
"fetch_from": "item_code.has_variants",
|
|
||||||
"fieldname": "has_variants",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"in_standard_filter": 1,
|
|
||||||
"label": "Has Variants",
|
|
||||||
"no_copy": 1,
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "variant_of",
|
|
||||||
"fetch_from": "item_code.variant_of",
|
|
||||||
"fieldname": "variant_of",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"ignore_user_permissions": 1,
|
|
||||||
"in_standard_filter": 1,
|
|
||||||
"label": "Variant Of",
|
|
||||||
"options": "Item",
|
|
||||||
"read_only": 1,
|
|
||||||
"search_index": 1,
|
|
||||||
"set_only_once": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fetch_from": "item_code.stock_uom",
|
|
||||||
"fieldname": "stock_uom",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"label": "Stock UOM",
|
|
||||||
"options": "UOM",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "brand",
|
|
||||||
"fetch_from": "item_code.brand",
|
|
||||||
"fieldname": "brand",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"label": "Brand",
|
|
||||||
"options": "Brand",
|
|
||||||
"search_index": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"collapsible": 1,
|
|
||||||
"fieldname": "advanced_display_section",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "Advanced Display Content"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "display_section",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "Display Images"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_27",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_22",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fetch_from": "item_code.description",
|
|
||||||
"fieldname": "description",
|
|
||||||
"fieldtype": "Text Editor",
|
|
||||||
"label": "Item Description",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "WEB-ITM-.####",
|
|
||||||
"fieldname": "naming_series",
|
|
||||||
"fieldtype": "Select",
|
|
||||||
"hidden": 1,
|
|
||||||
"label": "Naming Series",
|
|
||||||
"no_copy": 1,
|
|
||||||
"options": "WEB-ITM-.####",
|
|
||||||
"print_hide": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "display_additional_information_section",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "Display Additional Information"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"depends_on": "show_tabbed_section",
|
|
||||||
"fieldname": "tabs",
|
|
||||||
"fieldtype": "Table",
|
|
||||||
"label": "Tabs",
|
|
||||||
"options": "Website Item Tabbed Section"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fieldname": "show_tabbed_section",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Add Section with Tabs"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"collapsible": 1,
|
|
||||||
"fieldname": "offers_section",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "Offers"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "offers",
|
|
||||||
"fieldtype": "Table",
|
|
||||||
"label": "Offers to Display",
|
|
||||||
"options": "Website Offer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_11",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"description": "Short Description for List View",
|
|
||||||
"fieldname": "short_description",
|
|
||||||
"fieldtype": "Small Text",
|
|
||||||
"label": "Short Website Description"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"collapsible": 1,
|
|
||||||
"fieldname": "recommended_items_section",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "Recommended Items"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "recommended_items",
|
|
||||||
"fieldtype": "Table",
|
|
||||||
"label": "Recommended/Similar Items",
|
|
||||||
"options": "Recommended Items"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "stock_information_section",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "Stock Information"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_24",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"description": "Indicate that Item is available on backorder and not usually pre-stocked",
|
|
||||||
"fieldname": "on_backorder",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "On Backorder"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"has_web_view": 1,
|
|
||||||
"image_field": "website_image",
|
|
||||||
"index_web_pages_for_search": 1,
|
|
||||||
"links": [],
|
|
||||||
"make_attachments_public": 1,
|
|
||||||
"modified": "2023-09-12 14:19:22.822689",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"module": "E-commerce",
|
|
||||||
"name": "Website Item",
|
|
||||||
"naming_rule": "Expression (old style)",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"permissions": [
|
|
||||||
{
|
|
||||||
"create": 1,
|
|
||||||
"delete": 1,
|
|
||||||
"email": 1,
|
|
||||||
"export": 1,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
|
||||||
"report": 1,
|
|
||||||
"role": "System Manager",
|
|
||||||
"share": 1,
|
|
||||||
"write": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"create": 1,
|
|
||||||
"delete": 1,
|
|
||||||
"email": 1,
|
|
||||||
"export": 1,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
|
||||||
"report": 1,
|
|
||||||
"role": "Website Manager",
|
|
||||||
"share": 1,
|
|
||||||
"write": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"create": 1,
|
|
||||||
"delete": 1,
|
|
||||||
"email": 1,
|
|
||||||
"export": 1,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
|
||||||
"report": 1,
|
|
||||||
"role": "Stock User",
|
|
||||||
"share": 1,
|
|
||||||
"write": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"create": 1,
|
|
||||||
"delete": 1,
|
|
||||||
"email": 1,
|
|
||||||
"export": 1,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
|
||||||
"report": 1,
|
|
||||||
"role": "Stock Manager",
|
|
||||||
"share": 1,
|
|
||||||
"write": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"search_fields": "web_item_name, item_code, item_group",
|
|
||||||
"show_name_in_global_search": 1,
|
|
||||||
"sort_field": "modified",
|
|
||||||
"sort_order": "DESC",
|
|
||||||
"states": [],
|
|
||||||
"title_field": "web_item_name",
|
|
||||||
"track_changes": 1
|
|
||||||
}
|
|
@ -1,469 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
|
||||||
# For license information, please see license.txt
|
|
||||||
|
|
||||||
import json
|
|
||||||
from typing import TYPE_CHECKING, List, Union
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from erpnext.stock.doctype.item.item import Item
|
|
||||||
|
|
||||||
import frappe
|
|
||||||
from frappe import _
|
|
||||||
from frappe.utils import cint, cstr, flt, random_string
|
|
||||||
from frappe.website.doctype.website_slideshow.website_slideshow import get_slideshow
|
|
||||||
from frappe.website.website_generator import WebsiteGenerator
|
|
||||||
|
|
||||||
from erpnext.e_commerce.doctype.item_review.item_review import get_item_reviews
|
|
||||||
from erpnext.e_commerce.redisearch_utils import (
|
|
||||||
delete_item_from_index,
|
|
||||||
insert_item_to_index,
|
|
||||||
update_index_for_item,
|
|
||||||
)
|
|
||||||
from erpnext.e_commerce.shopping_cart.cart import _set_price_list
|
|
||||||
from erpnext.setup.doctype.item_group.item_group import (
|
|
||||||
get_parent_item_groups,
|
|
||||||
invalidate_cache_for,
|
|
||||||
)
|
|
||||||
from erpnext.utilities.product import get_price
|
|
||||||
|
|
||||||
|
|
||||||
class WebsiteItem(WebsiteGenerator):
|
|
||||||
website = frappe._dict(
|
|
||||||
page_title_field="web_item_name",
|
|
||||||
condition_field="published",
|
|
||||||
template="templates/generators/item/item.html",
|
|
||||||
no_cache=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
def autoname(self):
|
|
||||||
# use naming series to accomodate items with same name (different item code)
|
|
||||||
from frappe.model.naming import get_default_naming_series, make_autoname
|
|
||||||
|
|
||||||
naming_series = get_default_naming_series("Website Item")
|
|
||||||
if not self.name and naming_series:
|
|
||||||
self.name = make_autoname(naming_series, doc=self)
|
|
||||||
|
|
||||||
def onload(self):
|
|
||||||
super(WebsiteItem, self).onload()
|
|
||||||
|
|
||||||
def validate(self):
|
|
||||||
super(WebsiteItem, self).validate()
|
|
||||||
|
|
||||||
if not self.item_code:
|
|
||||||
frappe.throw(_("Item Code is required"), title=_("Mandatory"))
|
|
||||||
|
|
||||||
self.validate_duplicate_website_item()
|
|
||||||
self.validate_website_image()
|
|
||||||
self.make_thumbnail()
|
|
||||||
self.publish_unpublish_desk_item(publish=True)
|
|
||||||
|
|
||||||
if not self.get("__islocal"):
|
|
||||||
wig = frappe.qb.DocType("Website Item Group")
|
|
||||||
query = (
|
|
||||||
frappe.qb.from_(wig)
|
|
||||||
.select(wig.item_group)
|
|
||||||
.where(
|
|
||||||
(wig.parentfield == "website_item_groups")
|
|
||||||
& (wig.parenttype == "Website Item")
|
|
||||||
& (wig.parent == self.name)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
result = query.run(as_list=True)
|
|
||||||
|
|
||||||
self.old_website_item_groups = [x[0] for x in result]
|
|
||||||
|
|
||||||
def on_update(self):
|
|
||||||
invalidate_cache_for_web_item(self)
|
|
||||||
self.update_template_item()
|
|
||||||
|
|
||||||
def on_trash(self):
|
|
||||||
super(WebsiteItem, self).on_trash()
|
|
||||||
delete_item_from_index(self)
|
|
||||||
self.publish_unpublish_desk_item(publish=False)
|
|
||||||
|
|
||||||
def validate_duplicate_website_item(self):
|
|
||||||
existing_web_item = frappe.db.exists("Website Item", {"item_code": self.item_code})
|
|
||||||
if existing_web_item and existing_web_item != self.name:
|
|
||||||
message = _("Website Item already exists against Item {0}").format(frappe.bold(self.item_code))
|
|
||||||
frappe.throw(message, title=_("Already Published"))
|
|
||||||
|
|
||||||
def publish_unpublish_desk_item(self, publish=True):
|
|
||||||
if frappe.db.get_value("Item", self.item_code, "published_in_website") and publish:
|
|
||||||
return # if already published don't publish again
|
|
||||||
frappe.db.set_value("Item", self.item_code, "published_in_website", publish)
|
|
||||||
|
|
||||||
def make_route(self):
|
|
||||||
"""Called from set_route in WebsiteGenerator."""
|
|
||||||
if not self.route:
|
|
||||||
return (
|
|
||||||
cstr(frappe.db.get_value("Item Group", self.item_group, "route"))
|
|
||||||
+ "/"
|
|
||||||
+ self.scrub((self.item_name if self.item_name else self.item_code) + "-" + random_string(5))
|
|
||||||
)
|
|
||||||
|
|
||||||
def update_template_item(self):
|
|
||||||
"""Publish Template Item if Variant is published."""
|
|
||||||
if self.variant_of:
|
|
||||||
if self.published:
|
|
||||||
# show template
|
|
||||||
template_item = frappe.get_doc("Item", self.variant_of)
|
|
||||||
|
|
||||||
if not template_item.published_in_website:
|
|
||||||
template_item.flags.ignore_permissions = True
|
|
||||||
make_website_item(template_item)
|
|
||||||
|
|
||||||
def validate_website_image(self):
|
|
||||||
if frappe.flags.in_import:
|
|
||||||
return
|
|
||||||
|
|
||||||
"""Validate if the website image is a public file"""
|
|
||||||
if not self.website_image:
|
|
||||||
return
|
|
||||||
|
|
||||||
# find if website image url exists as public
|
|
||||||
file_doc = frappe.get_all(
|
|
||||||
"File",
|
|
||||||
filters={"file_url": self.website_image},
|
|
||||||
fields=["name", "is_private"],
|
|
||||||
order_by="is_private asc",
|
|
||||||
limit_page_length=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
if file_doc:
|
|
||||||
file_doc = file_doc[0]
|
|
||||||
|
|
||||||
if not file_doc:
|
|
||||||
frappe.msgprint(
|
|
||||||
_("Website Image {0} attached to Item {1} cannot be found").format(
|
|
||||||
self.website_image, self.name
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.website_image = None
|
|
||||||
|
|
||||||
elif file_doc.is_private:
|
|
||||||
frappe.msgprint(_("Website Image should be a public file or website URL"))
|
|
||||||
|
|
||||||
self.website_image = None
|
|
||||||
|
|
||||||
def make_thumbnail(self):
|
|
||||||
"""Make a thumbnail of `website_image`"""
|
|
||||||
if frappe.flags.in_import or frappe.flags.in_migrate:
|
|
||||||
return
|
|
||||||
|
|
||||||
import requests.exceptions
|
|
||||||
|
|
||||||
db_website_image = frappe.db.get_value(self.doctype, self.name, "website_image")
|
|
||||||
if not self.is_new() and self.website_image != db_website_image:
|
|
||||||
self.thumbnail = None
|
|
||||||
|
|
||||||
if self.website_image and not self.thumbnail:
|
|
||||||
file_doc = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
file_doc = frappe.get_doc(
|
|
||||||
"File",
|
|
||||||
{
|
|
||||||
"file_url": self.website_image,
|
|
||||||
"attached_to_doctype": "Website Item",
|
|
||||||
"attached_to_name": self.name,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
except frappe.DoesNotExistError:
|
|
||||||
pass
|
|
||||||
# cleanup
|
|
||||||
frappe.local.message_log.pop()
|
|
||||||
|
|
||||||
except requests.exceptions.HTTPError:
|
|
||||||
frappe.msgprint(_("Warning: Invalid attachment {0}").format(self.website_image))
|
|
||||||
self.website_image = None
|
|
||||||
|
|
||||||
except requests.exceptions.SSLError:
|
|
||||||
frappe.msgprint(
|
|
||||||
_("Warning: Invalid SSL certificate on attachment {0}").format(self.website_image)
|
|
||||||
)
|
|
||||||
self.website_image = None
|
|
||||||
|
|
||||||
# for CSV import
|
|
||||||
if self.website_image and not file_doc:
|
|
||||||
try:
|
|
||||||
file_doc = frappe.get_doc(
|
|
||||||
{
|
|
||||||
"doctype": "File",
|
|
||||||
"file_url": self.website_image,
|
|
||||||
"attached_to_doctype": "Website Item",
|
|
||||||
"attached_to_name": self.name,
|
|
||||||
}
|
|
||||||
).save()
|
|
||||||
|
|
||||||
except IOError:
|
|
||||||
self.website_image = None
|
|
||||||
|
|
||||||
if file_doc:
|
|
||||||
if not file_doc.thumbnail_url:
|
|
||||||
file_doc.make_thumbnail()
|
|
||||||
|
|
||||||
self.thumbnail = file_doc.thumbnail_url
|
|
||||||
|
|
||||||
def get_context(self, context):
|
|
||||||
context.show_search = True
|
|
||||||
context.search_link = "/search"
|
|
||||||
context.body_class = "product-page"
|
|
||||||
|
|
||||||
context.parents = get_parent_item_groups(self.item_group, from_item=True) # breadcumbs
|
|
||||||
self.attributes = frappe.get_all(
|
|
||||||
"Item Variant Attribute",
|
|
||||||
fields=["attribute", "attribute_value"],
|
|
||||||
filters={"parent": self.item_code},
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.slideshow:
|
|
||||||
context.update(get_slideshow(self))
|
|
||||||
|
|
||||||
self.set_metatags(context)
|
|
||||||
self.set_shopping_cart_data(context)
|
|
||||||
|
|
||||||
settings = context.shopping_cart.cart_settings
|
|
||||||
|
|
||||||
self.get_product_details_section(context)
|
|
||||||
|
|
||||||
if settings.get("enable_reviews"):
|
|
||||||
reviews_data = get_item_reviews(self.name)
|
|
||||||
context.update(reviews_data)
|
|
||||||
context.reviews = context.reviews[:4]
|
|
||||||
|
|
||||||
context.wished = False
|
|
||||||
if frappe.db.exists(
|
|
||||||
"Wishlist Item", {"item_code": self.item_code, "parent": frappe.session.user}
|
|
||||||
):
|
|
||||||
context.wished = True
|
|
||||||
|
|
||||||
context.user_is_customer = check_if_user_is_customer()
|
|
||||||
|
|
||||||
context.recommended_items = None
|
|
||||||
if settings and settings.enable_recommendations:
|
|
||||||
context.recommended_items = self.get_recommended_items(settings)
|
|
||||||
|
|
||||||
return context
|
|
||||||
|
|
||||||
def set_selected_attributes(self, variants, context, attribute_values_available):
|
|
||||||
for variant in variants:
|
|
||||||
variant.attributes = frappe.get_all(
|
|
||||||
"Item Variant Attribute",
|
|
||||||
filters={"parent": variant.name},
|
|
||||||
fields=["attribute", "attribute_value as value"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# make an attribute-value map for easier access in templates
|
|
||||||
variant.attribute_map = frappe._dict(
|
|
||||||
{attr.attribute: attr.value for attr in variant.attributes}
|
|
||||||
)
|
|
||||||
|
|
||||||
for attr in variant.attributes:
|
|
||||||
values = attribute_values_available.setdefault(attr.attribute, [])
|
|
||||||
if attr.value not in values:
|
|
||||||
values.append(attr.value)
|
|
||||||
|
|
||||||
if variant.name == context.variant.name:
|
|
||||||
context.selected_attributes[attr.attribute] = attr.value
|
|
||||||
|
|
||||||
def set_attribute_values(self, attributes, context, attribute_values_available):
|
|
||||||
for attr in attributes:
|
|
||||||
values = context.attribute_values.setdefault(attr.attribute, [])
|
|
||||||
|
|
||||||
if cint(frappe.db.get_value("Item Attribute", attr.attribute, "numeric_values")):
|
|
||||||
for val in sorted(attribute_values_available.get(attr.attribute, []), key=flt):
|
|
||||||
values.append(val)
|
|
||||||
else:
|
|
||||||
# get list of values defined (for sequence)
|
|
||||||
for attr_value in frappe.db.get_all(
|
|
||||||
"Item Attribute Value",
|
|
||||||
fields=["attribute_value"],
|
|
||||||
filters={"parent": attr.attribute},
|
|
||||||
order_by="idx asc",
|
|
||||||
):
|
|
||||||
|
|
||||||
if attr_value.attribute_value in attribute_values_available.get(attr.attribute, []):
|
|
||||||
values.append(attr_value.attribute_value)
|
|
||||||
|
|
||||||
def set_metatags(self, context):
|
|
||||||
context.metatags = frappe._dict({})
|
|
||||||
|
|
||||||
safe_description = frappe.utils.to_markdown(self.description)
|
|
||||||
|
|
||||||
context.metatags.url = frappe.utils.get_url() + "/" + context.route
|
|
||||||
|
|
||||||
if context.website_image:
|
|
||||||
if context.website_image.startswith("http"):
|
|
||||||
url = context.website_image
|
|
||||||
else:
|
|
||||||
url = frappe.utils.get_url() + context.website_image
|
|
||||||
context.metatags.image = url
|
|
||||||
|
|
||||||
context.metatags.description = safe_description[:300]
|
|
||||||
|
|
||||||
context.metatags.title = self.web_item_name or self.item_name or self.item_code
|
|
||||||
|
|
||||||
context.metatags["og:type"] = "product"
|
|
||||||
context.metatags["og:site_name"] = "ERPNext"
|
|
||||||
|
|
||||||
def set_shopping_cart_data(self, context):
|
|
||||||
from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website
|
|
||||||
|
|
||||||
context.shopping_cart = get_product_info_for_website(
|
|
||||||
self.item_code, skip_quotation_creation=True
|
|
||||||
)
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def copy_specification_from_item_group(self):
|
|
||||||
self.set("website_specifications", [])
|
|
||||||
if self.item_group:
|
|
||||||
for label, desc in frappe.db.get_values(
|
|
||||||
"Item Website Specification", {"parent": self.item_group}, ["label", "description"]
|
|
||||||
):
|
|
||||||
row = self.append("website_specifications")
|
|
||||||
row.label = label
|
|
||||||
row.description = desc
|
|
||||||
|
|
||||||
def get_product_details_section(self, context):
|
|
||||||
"""Get section with tabs or website specifications."""
|
|
||||||
context.show_tabs = self.show_tabbed_section
|
|
||||||
if self.show_tabbed_section and (self.tabs or self.website_specifications):
|
|
||||||
context.tabs = self.get_tabs()
|
|
||||||
else:
|
|
||||||
context.website_specifications = self.website_specifications
|
|
||||||
|
|
||||||
def get_tabs(self):
|
|
||||||
tab_values = {}
|
|
||||||
tab_values["tab_1_title"] = "Product Details"
|
|
||||||
tab_values["tab_1_content"] = frappe.render_template(
|
|
||||||
"templates/generators/item/item_specifications.html",
|
|
||||||
{"website_specifications": self.website_specifications, "show_tabs": self.show_tabbed_section},
|
|
||||||
)
|
|
||||||
|
|
||||||
for row in self.tabs:
|
|
||||||
tab_values[f"tab_{row.idx + 1}_title"] = _(row.label)
|
|
||||||
tab_values[f"tab_{row.idx + 1}_content"] = row.content
|
|
||||||
|
|
||||||
return tab_values
|
|
||||||
|
|
||||||
def get_recommended_items(self, settings):
|
|
||||||
ri = frappe.qb.DocType("Recommended Items")
|
|
||||||
wi = frappe.qb.DocType("Website Item")
|
|
||||||
|
|
||||||
query = (
|
|
||||||
frappe.qb.from_(ri)
|
|
||||||
.join(wi)
|
|
||||||
.on(ri.item_code == wi.item_code)
|
|
||||||
.select(ri.item_code, ri.route, ri.website_item_name, ri.website_item_thumbnail)
|
|
||||||
.where((ri.parent == self.name) & (wi.published == 1))
|
|
||||||
.orderby(ri.idx)
|
|
||||||
)
|
|
||||||
items = query.run(as_dict=True)
|
|
||||||
|
|
||||||
if settings.show_price:
|
|
||||||
is_guest = frappe.session.user == "Guest"
|
|
||||||
# Show Price if logged in.
|
|
||||||
# If not logged in and price is hidden for guest, skip price fetch.
|
|
||||||
if is_guest and settings.hide_price_for_guest:
|
|
||||||
return items
|
|
||||||
|
|
||||||
selling_price_list = _set_price_list(settings, None)
|
|
||||||
for item in items:
|
|
||||||
item.price_info = get_price(
|
|
||||||
item.item_code, selling_price_list, settings.default_customer_group, settings.company
|
|
||||||
)
|
|
||||||
|
|
||||||
return items
|
|
||||||
|
|
||||||
|
|
||||||
def invalidate_cache_for_web_item(doc):
|
|
||||||
"""Invalidate Website Item Group cache and rebuild ItemVariantsCacheManager."""
|
|
||||||
from erpnext.stock.doctype.item.item import invalidate_item_variants_cache_for_website
|
|
||||||
|
|
||||||
invalidate_cache_for(doc, doc.item_group)
|
|
||||||
|
|
||||||
website_item_groups = list(
|
|
||||||
set(
|
|
||||||
(doc.get("old_website_item_groups") or [])
|
|
||||||
+ [d.item_group for d in doc.get({"doctype": "Website Item Group"}) if d.item_group]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
for item_group in website_item_groups:
|
|
||||||
invalidate_cache_for(doc, item_group)
|
|
||||||
|
|
||||||
# Update Search Cache
|
|
||||||
update_index_for_item(doc)
|
|
||||||
|
|
||||||
invalidate_item_variants_cache_for_website(doc)
|
|
||||||
|
|
||||||
|
|
||||||
def on_doctype_update():
|
|
||||||
# since route is a Text column, it needs a length for indexing
|
|
||||||
frappe.db.add_index("Website Item", ["route(500)"])
|
|
||||||
|
|
||||||
|
|
||||||
def check_if_user_is_customer(user=None):
|
|
||||||
from frappe.contacts.doctype.contact.contact import get_contact_name
|
|
||||||
|
|
||||||
if not user:
|
|
||||||
user = frappe.session.user
|
|
||||||
|
|
||||||
contact_name = get_contact_name(user)
|
|
||||||
customer = None
|
|
||||||
|
|
||||||
if contact_name:
|
|
||||||
contact = frappe.get_doc("Contact", contact_name)
|
|
||||||
for link in contact.links:
|
|
||||||
if link.link_doctype == "Customer":
|
|
||||||
customer = link.link_name
|
|
||||||
break
|
|
||||||
|
|
||||||
return True if customer else False
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def make_website_item(doc: "Item", save: bool = True) -> Union["WebsiteItem", List[str]]:
|
|
||||||
"Make Website Item from Item. Used via Form UI or patch."
|
|
||||||
|
|
||||||
if not doc:
|
|
||||||
return
|
|
||||||
|
|
||||||
if isinstance(doc, str):
|
|
||||||
doc = json.loads(doc)
|
|
||||||
|
|
||||||
if frappe.db.exists("Website Item", {"item_code": doc.get("item_code")}):
|
|
||||||
message = _("Website Item already exists against {0}").format(frappe.bold(doc.get("item_code")))
|
|
||||||
frappe.throw(message, title=_("Already Published"))
|
|
||||||
|
|
||||||
website_item = frappe.new_doc("Website Item")
|
|
||||||
website_item.web_item_name = doc.get("item_name")
|
|
||||||
|
|
||||||
fields_to_map = [
|
|
||||||
"item_code",
|
|
||||||
"item_name",
|
|
||||||
"item_group",
|
|
||||||
"stock_uom",
|
|
||||||
"brand",
|
|
||||||
"has_variants",
|
|
||||||
"variant_of",
|
|
||||||
"description",
|
|
||||||
]
|
|
||||||
for field in fields_to_map:
|
|
||||||
website_item.update({field: doc.get(field)})
|
|
||||||
|
|
||||||
# Needed for publishing/mapping via Form UI only
|
|
||||||
if not frappe.flags.in_migrate and (doc.get("image") and not website_item.website_image):
|
|
||||||
website_item.website_image = doc.get("image")
|
|
||||||
|
|
||||||
if not save:
|
|
||||||
return website_item
|
|
||||||
|
|
||||||
website_item.save()
|
|
||||||
|
|
||||||
# Add to search cache
|
|
||||||
insert_item_to_index(website_item)
|
|
||||||
|
|
||||||
return [website_item.name, website_item.web_item_name]
|
|
@ -1,20 +0,0 @@
|
|||||||
frappe.listview_settings['Website Item'] = {
|
|
||||||
add_fields: ["item_name", "web_item_name", "published", "website_image", "has_variants", "variant_of"],
|
|
||||||
filters: [["published", "=", "1"]],
|
|
||||||
|
|
||||||
get_indicator: function(doc) {
|
|
||||||
if (doc.has_variants && doc.published) {
|
|
||||||
return [__("Template"), "orange", "has_variants,=,Yes|published,=,1"];
|
|
||||||
} else if (doc.has_variants && !doc.published) {
|
|
||||||
return [__("Template"), "grey", "has_variants,=,Yes|published,=,0"];
|
|
||||||
} else if (doc.variant_of && doc.published) {
|
|
||||||
return [__("Variant"), "blue", "published,=,1|variant_of,=," + doc.variant_of];
|
|
||||||
} else if (doc.variant_of && !doc.published) {
|
|
||||||
return [__("Variant"), "grey", "published,=,0|variant_of,=," + doc.variant_of];
|
|
||||||
} else if (doc.published) {
|
|
||||||
return [__("Published"), "green", "published,=,1"];
|
|
||||||
} else {
|
|
||||||
return [__("Not Published"), "grey", "published,=,0"];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
@ -1,37 +0,0 @@
|
|||||||
{
|
|
||||||
"actions": [],
|
|
||||||
"creation": "2021-03-18 20:32:15.321402",
|
|
||||||
"doctype": "DocType",
|
|
||||||
"editable_grid": 1,
|
|
||||||
"engine": "InnoDB",
|
|
||||||
"field_order": [
|
|
||||||
"label",
|
|
||||||
"content"
|
|
||||||
],
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldname": "label",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Label"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "content",
|
|
||||||
"fieldtype": "HTML Editor",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Content"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"index_web_pages_for_search": 1,
|
|
||||||
"istable": 1,
|
|
||||||
"links": [],
|
|
||||||
"modified": "2021-03-18 20:35:26.991192",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"module": "E-commerce",
|
|
||||||
"name": "Website Item Tabbed Section",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"permissions": [],
|
|
||||||
"sort_field": "modified",
|
|
||||||
"sort_order": "DESC",
|
|
||||||
"track_changes": 1
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
|
||||||
# For license information, please see license.txt
|
|
||||||
|
|
||||||
# import frappe
|
|
||||||
from frappe.model.document import Document
|
|
||||||
|
|
||||||
|
|
||||||
class WebsiteItemTabbedSection(Document):
|
|
||||||
pass
|
|
@ -1,43 +0,0 @@
|
|||||||
{
|
|
||||||
"actions": [],
|
|
||||||
"creation": "2021-04-21 13:37:14.162162",
|
|
||||||
"doctype": "DocType",
|
|
||||||
"editable_grid": 1,
|
|
||||||
"engine": "InnoDB",
|
|
||||||
"field_order": [
|
|
||||||
"offer_title",
|
|
||||||
"offer_subtitle",
|
|
||||||
"offer_details"
|
|
||||||
],
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldname": "offer_title",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Offer Title"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "offer_subtitle",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Offer Subtitle"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "offer_details",
|
|
||||||
"fieldtype": "Text Editor",
|
|
||||||
"label": "Offer Details"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"index_web_pages_for_search": 1,
|
|
||||||
"istable": 1,
|
|
||||||
"links": [],
|
|
||||||
"modified": "2021-04-21 13:56:04.660331",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"module": "E-commerce",
|
|
||||||
"name": "Website Offer",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"permissions": [],
|
|
||||||
"sort_field": "modified",
|
|
||||||
"sort_order": "DESC",
|
|
||||||
"track_changes": 1
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
|
||||||
# For license information, please see license.txt
|
|
||||||
|
|
||||||
import frappe
|
|
||||||
from frappe.model.document import Document
|
|
||||||
|
|
||||||
|
|
||||||
class WebsiteOffer(Document):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
|
||||||
def get_offer_details(offer_id):
|
|
||||||
return frappe.db.get_value("Website Offer", {"name": offer_id}, ["offer_details"])
|
|
@ -1,117 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
|
||||||
# See license.txt
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import frappe
|
|
||||||
from frappe.core.doctype.user_permission.test_user_permission import create_user
|
|
||||||
|
|
||||||
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
|
|
||||||
from erpnext.e_commerce.doctype.wishlist.wishlist import add_to_wishlist, remove_from_wishlist
|
|
||||||
from erpnext.stock.doctype.item.test_item import make_item
|
|
||||||
|
|
||||||
|
|
||||||
class TestWishlist(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
item = make_item("Test Phone Series X")
|
|
||||||
if not frappe.db.exists("Website Item", {"item_code": "Test Phone Series X"}):
|
|
||||||
make_website_item(item, save=True)
|
|
||||||
|
|
||||||
item = make_item("Test Phone Series Y")
|
|
||||||
if not frappe.db.exists("Website Item", {"item_code": "Test Phone Series Y"}):
|
|
||||||
make_website_item(item, save=True)
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
frappe.get_cached_doc("Website Item", {"item_code": "Test Phone Series X"}).delete()
|
|
||||||
frappe.get_cached_doc("Website Item", {"item_code": "Test Phone Series Y"}).delete()
|
|
||||||
frappe.get_cached_doc("Item", "Test Phone Series X").delete()
|
|
||||||
frappe.get_cached_doc("Item", "Test Phone Series Y").delete()
|
|
||||||
|
|
||||||
def test_add_remove_items_in_wishlist(self):
|
|
||||||
"Check if items are added and removed from user's wishlist."
|
|
||||||
# add first item
|
|
||||||
add_to_wishlist("Test Phone Series X")
|
|
||||||
|
|
||||||
# check if wishlist was created and item was added
|
|
||||||
self.assertTrue(frappe.db.exists("Wishlist", {"user": frappe.session.user}))
|
|
||||||
self.assertTrue(
|
|
||||||
frappe.db.exists(
|
|
||||||
"Wishlist Item", {"item_code": "Test Phone Series X", "parent": frappe.session.user}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# add second item to wishlist
|
|
||||||
add_to_wishlist("Test Phone Series Y")
|
|
||||||
wishlist_length = frappe.db.get_value(
|
|
||||||
"Wishlist Item", {"parent": frappe.session.user}, "count(*)"
|
|
||||||
)
|
|
||||||
self.assertEqual(wishlist_length, 2)
|
|
||||||
|
|
||||||
remove_from_wishlist("Test Phone Series X")
|
|
||||||
remove_from_wishlist("Test Phone Series Y")
|
|
||||||
|
|
||||||
wishlist_length = frappe.db.get_value(
|
|
||||||
"Wishlist Item", {"parent": frappe.session.user}, "count(*)"
|
|
||||||
)
|
|
||||||
self.assertIsNone(frappe.db.exists("Wishlist Item", {"parent": frappe.session.user}))
|
|
||||||
self.assertEqual(wishlist_length, 0)
|
|
||||||
|
|
||||||
# tear down
|
|
||||||
frappe.get_doc("Wishlist", {"user": frappe.session.user}).delete()
|
|
||||||
|
|
||||||
def test_add_remove_in_wishlist_multiple_users(self):
|
|
||||||
"Check if items are added and removed from the correct user's wishlist."
|
|
||||||
test_user = create_user("test_reviewer@example.com", "Customer")
|
|
||||||
test_user_1 = create_user("test_reviewer_1@example.com", "Customer")
|
|
||||||
|
|
||||||
# add to wishlist for first user
|
|
||||||
frappe.set_user(test_user.name)
|
|
||||||
add_to_wishlist("Test Phone Series X")
|
|
||||||
|
|
||||||
# add to wishlist for second user
|
|
||||||
frappe.set_user(test_user_1.name)
|
|
||||||
add_to_wishlist("Test Phone Series X")
|
|
||||||
|
|
||||||
# check wishlist and its content for users
|
|
||||||
self.assertTrue(frappe.db.exists("Wishlist", {"user": test_user.name}))
|
|
||||||
self.assertTrue(
|
|
||||||
frappe.db.exists(
|
|
||||||
"Wishlist Item", {"item_code": "Test Phone Series X", "parent": test_user.name}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertTrue(frappe.db.exists("Wishlist", {"user": test_user_1.name}))
|
|
||||||
self.assertTrue(
|
|
||||||
frappe.db.exists(
|
|
||||||
"Wishlist Item", {"item_code": "Test Phone Series X", "parent": test_user_1.name}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# remove item for second user
|
|
||||||
remove_from_wishlist("Test Phone Series X")
|
|
||||||
|
|
||||||
# make sure item was removed for second user and not first
|
|
||||||
self.assertFalse(
|
|
||||||
frappe.db.exists(
|
|
||||||
"Wishlist Item", {"item_code": "Test Phone Series X", "parent": test_user_1.name}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.assertTrue(
|
|
||||||
frappe.db.exists(
|
|
||||||
"Wishlist Item", {"item_code": "Test Phone Series X", "parent": test_user.name}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# remove item for first user
|
|
||||||
frappe.set_user(test_user.name)
|
|
||||||
remove_from_wishlist("Test Phone Series X")
|
|
||||||
self.assertFalse(
|
|
||||||
frappe.db.exists(
|
|
||||||
"Wishlist Item", {"item_code": "Test Phone Series X", "parent": test_user.name}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# tear down
|
|
||||||
frappe.set_user("Administrator")
|
|
||||||
frappe.get_doc("Wishlist", {"user": test_user.name}).delete()
|
|
||||||
frappe.get_doc("Wishlist", {"user": test_user_1.name}).delete()
|
|
@ -1,8 +0,0 @@
|
|||||||
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
|
||||||
// For license information, please see license.txt
|
|
||||||
|
|
||||||
frappe.ui.form.on('Wishlist', {
|
|
||||||
// refresh: function(frm) {
|
|
||||||
|
|
||||||
// }
|
|
||||||
});
|
|
@ -1,65 +0,0 @@
|
|||||||
{
|
|
||||||
"actions": [],
|
|
||||||
"autoname": "field:user",
|
|
||||||
"creation": "2021-03-10 18:52:28.769126",
|
|
||||||
"doctype": "DocType",
|
|
||||||
"editable_grid": 1,
|
|
||||||
"engine": "InnoDB",
|
|
||||||
"field_order": [
|
|
||||||
"user",
|
|
||||||
"section_break_2",
|
|
||||||
"items"
|
|
||||||
],
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fieldname": "user",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "User",
|
|
||||||
"options": "User",
|
|
||||||
"reqd": 1,
|
|
||||||
"unique": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "section_break_2",
|
|
||||||
"fieldtype": "Section Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "items",
|
|
||||||
"fieldtype": "Table",
|
|
||||||
"label": "Items",
|
|
||||||
"options": "Wishlist Item"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"in_create": 1,
|
|
||||||
"index_web_pages_for_search": 1,
|
|
||||||
"links": [],
|
|
||||||
"modified": "2021-07-08 13:11:21.693956",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"module": "E-commerce",
|
|
||||||
"name": "Wishlist",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"permissions": [
|
|
||||||
{
|
|
||||||
"email": 1,
|
|
||||||
"export": 1,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
|
||||||
"report": 1,
|
|
||||||
"role": "System Manager",
|
|
||||||
"share": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"email": 1,
|
|
||||||
"export": 1,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
|
||||||
"report": 1,
|
|
||||||
"role": "Website Manager",
|
|
||||||
"share": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"sort_field": "modified",
|
|
||||||
"sort_order": "DESC",
|
|
||||||
"track_changes": 1
|
|
||||||
}
|
|
@ -1,70 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
|
||||||
# For license information, please see license.txt
|
|
||||||
|
|
||||||
import frappe
|
|
||||||
from frappe.model.document import Document
|
|
||||||
|
|
||||||
|
|
||||||
class Wishlist(Document):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def add_to_wishlist(item_code):
|
|
||||||
"""Insert Item into wishlist."""
|
|
||||||
|
|
||||||
if frappe.db.exists("Wishlist Item", {"item_code": item_code, "parent": frappe.session.user}):
|
|
||||||
return
|
|
||||||
|
|
||||||
web_item_data = frappe.db.get_value(
|
|
||||||
"Website Item",
|
|
||||||
{"item_code": item_code},
|
|
||||||
[
|
|
||||||
"website_image",
|
|
||||||
"website_warehouse",
|
|
||||||
"name",
|
|
||||||
"web_item_name",
|
|
||||||
"item_name",
|
|
||||||
"item_group",
|
|
||||||
"route",
|
|
||||||
],
|
|
||||||
as_dict=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
wished_item_dict = {
|
|
||||||
"item_code": item_code,
|
|
||||||
"item_name": web_item_data.get("item_name"),
|
|
||||||
"item_group": web_item_data.get("item_group"),
|
|
||||||
"website_item": web_item_data.get("name"),
|
|
||||||
"web_item_name": web_item_data.get("web_item_name"),
|
|
||||||
"image": web_item_data.get("website_image"),
|
|
||||||
"warehouse": web_item_data.get("website_warehouse"),
|
|
||||||
"route": web_item_data.get("route"),
|
|
||||||
}
|
|
||||||
|
|
||||||
if not frappe.db.exists("Wishlist", frappe.session.user):
|
|
||||||
# initialise wishlist
|
|
||||||
wishlist = frappe.get_doc({"doctype": "Wishlist"})
|
|
||||||
wishlist.user = frappe.session.user
|
|
||||||
wishlist.append("items", wished_item_dict)
|
|
||||||
wishlist.save(ignore_permissions=True)
|
|
||||||
else:
|
|
||||||
wishlist = frappe.get_doc("Wishlist", frappe.session.user)
|
|
||||||
item = wishlist.append("items", wished_item_dict)
|
|
||||||
item.db_insert()
|
|
||||||
|
|
||||||
if hasattr(frappe.local, "cookie_manager"):
|
|
||||||
frappe.local.cookie_manager.set_cookie("wish_count", str(len(wishlist.items)))
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def remove_from_wishlist(item_code):
|
|
||||||
if frappe.db.exists("Wishlist Item", {"item_code": item_code, "parent": frappe.session.user}):
|
|
||||||
frappe.db.delete("Wishlist Item", {"item_code": item_code, "parent": frappe.session.user})
|
|
||||||
frappe.db.commit() # nosemgrep
|
|
||||||
|
|
||||||
wishlist_items = frappe.db.get_values("Wishlist Item", filters={"parent": frappe.session.user})
|
|
||||||
|
|
||||||
if hasattr(frappe.local, "cookie_manager"):
|
|
||||||
frappe.local.cookie_manager.set_cookie("wish_count", str(len(wishlist_items)))
|
|
@ -1,147 +0,0 @@
|
|||||||
{
|
|
||||||
"actions": [],
|
|
||||||
"creation": "2021-03-10 19:03:00.662714",
|
|
||||||
"doctype": "DocType",
|
|
||||||
"editable_grid": 1,
|
|
||||||
"engine": "InnoDB",
|
|
||||||
"field_order": [
|
|
||||||
"item_code",
|
|
||||||
"website_item",
|
|
||||||
"web_item_name",
|
|
||||||
"column_break_3",
|
|
||||||
"item_name",
|
|
||||||
"item_group",
|
|
||||||
"item_details_section",
|
|
||||||
"description",
|
|
||||||
"column_break_7",
|
|
||||||
"route",
|
|
||||||
"image",
|
|
||||||
"image_view",
|
|
||||||
"section_break_8",
|
|
||||||
"warehouse_section",
|
|
||||||
"warehouse"
|
|
||||||
],
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"fetch_from": "website_item.item_code",
|
|
||||||
"fetch_if_empty": 1,
|
|
||||||
"fieldname": "item_code",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Item Code",
|
|
||||||
"options": "Item",
|
|
||||||
"reqd": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "website_item",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Website Item",
|
|
||||||
"options": "Website Item",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_3",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fetch_from": "item_code.item_name",
|
|
||||||
"fetch_if_empty": 1,
|
|
||||||
"fieldname": "item_name",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "Item Name",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"collapsible": 1,
|
|
||||||
"fieldname": "item_details_section",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "Item Details",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fetch_from": "item_code.description",
|
|
||||||
"fetch_if_empty": 1,
|
|
||||||
"fieldname": "description",
|
|
||||||
"fieldtype": "Text Editor",
|
|
||||||
"label": "Description",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_7",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fetch_from": "item_code.image",
|
|
||||||
"fetch_if_empty": 1,
|
|
||||||
"fieldname": "image",
|
|
||||||
"fieldtype": "Attach",
|
|
||||||
"hidden": 1,
|
|
||||||
"label": "Image"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fetch_from": "item_code.image",
|
|
||||||
"fetch_if_empty": 1,
|
|
||||||
"fieldname": "image_view",
|
|
||||||
"fieldtype": "Image",
|
|
||||||
"hidden": 1,
|
|
||||||
"label": "Image View",
|
|
||||||
"options": "image",
|
|
||||||
"print_hide": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "warehouse_section",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"label": "Warehouse"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "warehouse",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Warehouse",
|
|
||||||
"options": "Warehouse",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "section_break_8",
|
|
||||||
"fieldtype": "Section Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fetch_from": "item_code.item_group",
|
|
||||||
"fetch_if_empty": 1,
|
|
||||||
"fieldname": "item_group",
|
|
||||||
"fieldtype": "Link",
|
|
||||||
"label": "Item Group",
|
|
||||||
"options": "Item Group",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fetch_from": "website_item.route",
|
|
||||||
"fetch_if_empty": 1,
|
|
||||||
"fieldname": "route",
|
|
||||||
"fieldtype": "Small Text",
|
|
||||||
"label": "Route",
|
|
||||||
"read_only": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fetch_from": "website_item.web_item_name",
|
|
||||||
"fetch_if_empty": 1,
|
|
||||||
"fieldname": "web_item_name",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "Website Item Name",
|
|
||||||
"read_only": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"index_web_pages_for_search": 1,
|
|
||||||
"istable": 1,
|
|
||||||
"links": [],
|
|
||||||
"modified": "2021-08-09 10:30:41.964802",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"module": "E-commerce",
|
|
||||||
"name": "Wishlist Item",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"permissions": [],
|
|
||||||
"sort_field": "modified",
|
|
||||||
"sort_order": "DESC",
|
|
||||||
"track_changes": 1
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
|
||||||
# For license information, please see license.txt
|
|
||||||
|
|
||||||
# import frappe
|
|
||||||
from frappe.model.document import Document
|
|
||||||
|
|
||||||
|
|
||||||
class WishlistItem(Document):
|
|
||||||
pass
|
|
@ -1,134 +0,0 @@
|
|||||||
import frappe
|
|
||||||
from frappe.search.full_text_search import FullTextSearch
|
|
||||||
from frappe.utils import strip_html_tags
|
|
||||||
from whoosh.analysis import StemmingAnalyzer
|
|
||||||
from whoosh.fields import ID, KEYWORD, TEXT, Schema
|
|
||||||
from whoosh.qparser import FieldsPlugin, MultifieldParser, WildcardPlugin
|
|
||||||
from whoosh.query import Prefix
|
|
||||||
|
|
||||||
# TODO: Make obsolete
|
|
||||||
INDEX_NAME = "products"
|
|
||||||
|
|
||||||
|
|
||||||
class ProductSearch(FullTextSearch):
|
|
||||||
"""Wrapper for WebsiteSearch"""
|
|
||||||
|
|
||||||
def get_schema(self):
|
|
||||||
return Schema(
|
|
||||||
title=TEXT(stored=True, field_boost=1.5),
|
|
||||||
name=ID(stored=True),
|
|
||||||
path=ID(stored=True),
|
|
||||||
content=TEXT(stored=True, analyzer=StemmingAnalyzer()),
|
|
||||||
keywords=KEYWORD(stored=True, scorable=True, commas=True),
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_id(self):
|
|
||||||
return "name"
|
|
||||||
|
|
||||||
def get_items_to_index(self):
|
|
||||||
"""Get all routes to be indexed, this includes the static pages
|
|
||||||
in www/ and routes from published documents
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
self (object): FullTextSearch Instance
|
|
||||||
"""
|
|
||||||
items = get_all_published_items()
|
|
||||||
documents = [self.get_document_to_index(item) for item in items]
|
|
||||||
return documents
|
|
||||||
|
|
||||||
def get_document_to_index(self, item):
|
|
||||||
try:
|
|
||||||
item = frappe.get_doc("Item", item)
|
|
||||||
title = item.item_name
|
|
||||||
keywords = [item.item_group]
|
|
||||||
|
|
||||||
if item.brand:
|
|
||||||
keywords.append(item.brand)
|
|
||||||
|
|
||||||
if item.website_image_alt:
|
|
||||||
keywords.append(item.website_image_alt)
|
|
||||||
|
|
||||||
if item.has_variants and item.variant_based_on == "Item Attribute":
|
|
||||||
keywords = keywords + [attr.attribute for attr in item.attributes]
|
|
||||||
|
|
||||||
if item.web_long_description:
|
|
||||||
content = strip_html_tags(item.web_long_description)
|
|
||||||
elif item.description:
|
|
||||||
content = strip_html_tags(item.description)
|
|
||||||
|
|
||||||
return frappe._dict(
|
|
||||||
title=title,
|
|
||||||
name=item.name,
|
|
||||||
path=item.route,
|
|
||||||
content=content,
|
|
||||||
keywords=", ".join(keywords),
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def search(self, text, scope=None, limit=20):
|
|
||||||
"""Search from the current index
|
|
||||||
|
|
||||||
Args:
|
|
||||||
text (str): String to search for
|
|
||||||
scope (str, optional): Scope to limit the search. Defaults to None.
|
|
||||||
limit (int, optional): Limit number of search results. Defaults to 20.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
[List(_dict)]: Search results
|
|
||||||
"""
|
|
||||||
ix = self.get_index()
|
|
||||||
|
|
||||||
results = None
|
|
||||||
out = []
|
|
||||||
|
|
||||||
with ix.searcher() as searcher:
|
|
||||||
parser = MultifieldParser(["title", "content", "keywords"], ix.schema)
|
|
||||||
parser.remove_plugin_class(FieldsPlugin)
|
|
||||||
parser.remove_plugin_class(WildcardPlugin)
|
|
||||||
query = parser.parse(text)
|
|
||||||
|
|
||||||
filter_scoped = None
|
|
||||||
if scope:
|
|
||||||
filter_scoped = Prefix(self.id, scope)
|
|
||||||
results = searcher.search(query, limit=limit, filter=filter_scoped)
|
|
||||||
|
|
||||||
for r in results:
|
|
||||||
out.append(self.parse_result(r))
|
|
||||||
|
|
||||||
return out
|
|
||||||
|
|
||||||
def parse_result(self, result):
|
|
||||||
title_highlights = result.highlights("title")
|
|
||||||
content_highlights = result.highlights("content")
|
|
||||||
keyword_highlights = result.highlights("keywords")
|
|
||||||
|
|
||||||
return frappe._dict(
|
|
||||||
title=result["title"],
|
|
||||||
path=result["path"],
|
|
||||||
keywords=result["keywords"],
|
|
||||||
title_highlights=title_highlights,
|
|
||||||
content_highlights=content_highlights,
|
|
||||||
keyword_highlights=keyword_highlights,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_all_published_items():
|
|
||||||
return frappe.get_all(
|
|
||||||
"Website Item", filters={"variant_of": "", "published": 1}, pluck="item_code"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def update_index_for_path(path):
|
|
||||||
search = ProductSearch(INDEX_NAME)
|
|
||||||
return search.update_index_by_name(path)
|
|
||||||
|
|
||||||
|
|
||||||
def remove_document_from_index(path):
|
|
||||||
search = ProductSearch(INDEX_NAME)
|
|
||||||
return search.remove_document_from_index(path)
|
|
||||||
|
|
||||||
|
|
||||||
def build_index_for_all_routes():
|
|
||||||
search = ProductSearch(INDEX_NAME)
|
|
||||||
return search.build()
|
|
@ -1,158 +0,0 @@
|
|||||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
|
||||||
# License: GNU General Public License v3. See license.txt
|
|
||||||
import frappe
|
|
||||||
from frappe.utils import floor
|
|
||||||
|
|
||||||
|
|
||||||
class ProductFiltersBuilder:
|
|
||||||
def __init__(self, item_group=None):
|
|
||||||
if not item_group:
|
|
||||||
self.doc = frappe.get_doc("E Commerce Settings")
|
|
||||||
else:
|
|
||||||
self.doc = frappe.get_doc("Item Group", item_group)
|
|
||||||
|
|
||||||
self.item_group = item_group
|
|
||||||
|
|
||||||
def get_field_filters(self):
|
|
||||||
from erpnext.setup.doctype.item_group.item_group import get_child_groups_for_website
|
|
||||||
|
|
||||||
if not self.item_group and not self.doc.enable_field_filters:
|
|
||||||
return
|
|
||||||
|
|
||||||
fields, filter_data = [], []
|
|
||||||
filter_fields = [row.fieldname for row in self.doc.filter_fields] # fields in settings
|
|
||||||
|
|
||||||
# filter valid field filters i.e. those that exist in Website Item
|
|
||||||
web_item_meta = frappe.get_meta("Website Item", cached=True)
|
|
||||||
fields = [
|
|
||||||
web_item_meta.get_field(field) for field in filter_fields if web_item_meta.has_field(field)
|
|
||||||
]
|
|
||||||
|
|
||||||
for df in fields:
|
|
||||||
item_filters, item_or_filters = {"published": 1}, []
|
|
||||||
link_doctype_values = self.get_filtered_link_doctype_records(df)
|
|
||||||
|
|
||||||
if df.fieldtype == "Link":
|
|
||||||
if self.item_group:
|
|
||||||
include_child = frappe.db.get_value("Item Group", self.item_group, "include_descendants")
|
|
||||||
if include_child:
|
|
||||||
include_groups = get_child_groups_for_website(self.item_group, include_self=True)
|
|
||||||
include_groups = [x.name for x in include_groups]
|
|
||||||
item_or_filters.extend(
|
|
||||||
[
|
|
||||||
["item_group", "in", include_groups],
|
|
||||||
["Website Item Group", "item_group", "=", self.item_group], # consider website item groups
|
|
||||||
]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
item_or_filters.extend(
|
|
||||||
[
|
|
||||||
["item_group", "=", self.item_group],
|
|
||||||
["Website Item Group", "item_group", "=", self.item_group], # consider website item groups
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
# exclude variants if mentioned in settings
|
|
||||||
if frappe.db.get_single_value("E Commerce Settings", "hide_variants"):
|
|
||||||
item_filters["variant_of"] = ["is", "not set"]
|
|
||||||
|
|
||||||
# Get link field values attached to published items
|
|
||||||
item_values = frappe.get_all(
|
|
||||||
"Website Item",
|
|
||||||
fields=[df.fieldname],
|
|
||||||
filters=item_filters,
|
|
||||||
or_filters=item_or_filters,
|
|
||||||
distinct="True",
|
|
||||||
pluck=df.fieldname,
|
|
||||||
)
|
|
||||||
|
|
||||||
values = list(set(item_values) & link_doctype_values) # intersection of both
|
|
||||||
else:
|
|
||||||
# table multiselect
|
|
||||||
values = list(link_doctype_values)
|
|
||||||
|
|
||||||
# Remove None
|
|
||||||
if None in values:
|
|
||||||
values.remove(None)
|
|
||||||
|
|
||||||
if values:
|
|
||||||
filter_data.append([df, values])
|
|
||||||
|
|
||||||
return filter_data
|
|
||||||
|
|
||||||
def get_filtered_link_doctype_records(self, field):
|
|
||||||
"""
|
|
||||||
Get valid link doctype records depending on filters.
|
|
||||||
Apply enable/disable/show_in_website filter.
|
|
||||||
Returns:
|
|
||||||
set: A set containing valid record names
|
|
||||||
"""
|
|
||||||
link_doctype = field.get_link_doctype()
|
|
||||||
meta = frappe.get_meta(link_doctype, cached=True) if link_doctype else None
|
|
||||||
if meta:
|
|
||||||
filters = self.get_link_doctype_filters(meta)
|
|
||||||
link_doctype_values = set(d.name for d in frappe.get_all(link_doctype, filters))
|
|
||||||
|
|
||||||
return link_doctype_values if meta else set()
|
|
||||||
|
|
||||||
def get_link_doctype_filters(self, meta):
|
|
||||||
"Filters for Link Doctype eg. 'show_in_website'."
|
|
||||||
filters = {}
|
|
||||||
if not meta:
|
|
||||||
return filters
|
|
||||||
|
|
||||||
if meta.has_field("enabled"):
|
|
||||||
filters["enabled"] = 1
|
|
||||||
if meta.has_field("disabled"):
|
|
||||||
filters["disabled"] = 0
|
|
||||||
if meta.has_field("show_in_website"):
|
|
||||||
filters["show_in_website"] = 1
|
|
||||||
|
|
||||||
return filters
|
|
||||||
|
|
||||||
def get_attribute_filters(self):
|
|
||||||
if not self.item_group and not self.doc.enable_attribute_filters:
|
|
||||||
return
|
|
||||||
|
|
||||||
attributes = [row.attribute for row in self.doc.filter_attributes]
|
|
||||||
|
|
||||||
if not attributes:
|
|
||||||
return []
|
|
||||||
|
|
||||||
result = frappe.get_all(
|
|
||||||
"Item Variant Attribute",
|
|
||||||
filters={"attribute": ["in", attributes], "attribute_value": ["is", "set"]},
|
|
||||||
fields=["attribute", "attribute_value"],
|
|
||||||
distinct=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
attribute_value_map = {}
|
|
||||||
for d in result:
|
|
||||||
attribute_value_map.setdefault(d.attribute, []).append(d.attribute_value)
|
|
||||||
|
|
||||||
out = []
|
|
||||||
for name, values in attribute_value_map.items():
|
|
||||||
out.append(frappe._dict(name=name, item_attribute_values=values))
|
|
||||||
return out
|
|
||||||
|
|
||||||
def get_discount_filters(self, discounts):
|
|
||||||
discount_filters = []
|
|
||||||
|
|
||||||
# [25.89, 60.5] min max
|
|
||||||
min_discount, max_discount = discounts[0], discounts[1]
|
|
||||||
# [25, 60] rounded min max
|
|
||||||
min_range_absolute, max_range_absolute = floor(min_discount), floor(max_discount)
|
|
||||||
|
|
||||||
min_range = int(min_discount - (min_range_absolute % 10)) # 20
|
|
||||||
max_range = int(max_discount - (max_range_absolute % 10)) # 60
|
|
||||||
|
|
||||||
min_range = (
|
|
||||||
(min_range + 10) if min_range != min_range_absolute else min_range
|
|
||||||
) # 30 (upper limit of 25.89 in range of 10)
|
|
||||||
max_range = (max_range + 10) if max_range != max_range_absolute else max_range # 60
|
|
||||||
|
|
||||||
for discount in range(min_range, (max_range + 1), 10):
|
|
||||||
label = f"{discount}% and below"
|
|
||||||
discount_filters.append([discount, label])
|
|
||||||
|
|
||||||
return discount_filters
|
|
@ -1,321 +0,0 @@
|
|||||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
|
||||||
# License: GNU General Public License v3. See license.txt
|
|
||||||
|
|
||||||
import frappe
|
|
||||||
from frappe.utils import flt
|
|
||||||
|
|
||||||
from erpnext.e_commerce.doctype.item_review.item_review import get_customer
|
|
||||||
from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website
|
|
||||||
from erpnext.utilities.product import get_non_stock_item_status
|
|
||||||
|
|
||||||
|
|
||||||
class ProductQuery:
|
|
||||||
"""Query engine for product listing
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
fields (list): Fields to fetch in query
|
|
||||||
conditions (string): Conditions for query building
|
|
||||||
or_conditions (string): Search conditions
|
|
||||||
page_length (Int): Length of page for the query
|
|
||||||
settings (Document): E Commerce Settings DocType
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.settings = frappe.get_doc("E Commerce Settings")
|
|
||||||
self.page_length = self.settings.products_per_page or 20
|
|
||||||
|
|
||||||
self.or_filters = []
|
|
||||||
self.filters = [["published", "=", 1]]
|
|
||||||
self.fields = [
|
|
||||||
"web_item_name",
|
|
||||||
"name",
|
|
||||||
"item_name",
|
|
||||||
"item_code",
|
|
||||||
"website_image",
|
|
||||||
"variant_of",
|
|
||||||
"has_variants",
|
|
||||||
"item_group",
|
|
||||||
"web_long_description",
|
|
||||||
"short_description",
|
|
||||||
"route",
|
|
||||||
"website_warehouse",
|
|
||||||
"ranking",
|
|
||||||
"on_backorder",
|
|
||||||
]
|
|
||||||
|
|
||||||
def query(self, attributes=None, fields=None, search_term=None, start=0, item_group=None):
|
|
||||||
"""
|
|
||||||
Args:
|
|
||||||
attributes (dict, optional): Item Attribute filters
|
|
||||||
fields (dict, optional): Field level filters
|
|
||||||
search_term (str, optional): Search term to lookup
|
|
||||||
start (int, optional): Page start
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Dict containing items, item count & discount range
|
|
||||||
"""
|
|
||||||
# track if discounts included in field filters
|
|
||||||
self.filter_with_discount = bool(fields.get("discount"))
|
|
||||||
result, discount_list, website_item_groups, cart_items, count = [], [], [], [], 0
|
|
||||||
|
|
||||||
if fields:
|
|
||||||
self.build_fields_filters(fields)
|
|
||||||
if item_group:
|
|
||||||
self.build_item_group_filters(item_group)
|
|
||||||
if search_term:
|
|
||||||
self.build_search_filters(search_term)
|
|
||||||
if self.settings.hide_variants:
|
|
||||||
self.filters.append(["variant_of", "is", "not set"])
|
|
||||||
|
|
||||||
# query results
|
|
||||||
if attributes:
|
|
||||||
result, count = self.query_items_with_attributes(attributes, start)
|
|
||||||
else:
|
|
||||||
result, count = self.query_items(start=start)
|
|
||||||
|
|
||||||
# sort combined results by ranking
|
|
||||||
result = sorted(result, key=lambda x: x.get("ranking"), reverse=True)
|
|
||||||
|
|
||||||
if self.settings.enabled:
|
|
||||||
cart_items = self.get_cart_items()
|
|
||||||
|
|
||||||
result, discount_list = self.add_display_details(result, discount_list, cart_items)
|
|
||||||
|
|
||||||
discounts = []
|
|
||||||
if discount_list:
|
|
||||||
discounts = [min(discount_list), max(discount_list)]
|
|
||||||
|
|
||||||
result = self.filter_results_by_discount(fields, result)
|
|
||||||
|
|
||||||
return {"items": result, "items_count": count, "discounts": discounts}
|
|
||||||
|
|
||||||
def query_items(self, start=0):
|
|
||||||
"""Build a query to fetch Website Items based on field filters."""
|
|
||||||
# MySQL does not support offset without limit,
|
|
||||||
# frappe does not accept two parameters for limit
|
|
||||||
# https://dev.mysql.com/doc/refman/8.0/en/select.html#id4651989
|
|
||||||
count_items = frappe.db.get_all(
|
|
||||||
"Website Item",
|
|
||||||
filters=self.filters,
|
|
||||||
or_filters=self.or_filters,
|
|
||||||
limit_page_length=184467440737095516,
|
|
||||||
limit_start=start, # get all items from this offset for total count ahead
|
|
||||||
order_by="ranking desc",
|
|
||||||
)
|
|
||||||
count = len(count_items)
|
|
||||||
|
|
||||||
# If discounts included, return all rows.
|
|
||||||
# Slice after filtering rows with discount (See `filter_results_by_discount`).
|
|
||||||
# Slicing before hand will miss discounted items on the 3rd or 4th page.
|
|
||||||
# Discounts are fetched on computing Pricing Rules so we cannot query them directly.
|
|
||||||
page_length = 184467440737095516 if self.filter_with_discount else self.page_length
|
|
||||||
|
|
||||||
items = frappe.db.get_all(
|
|
||||||
"Website Item",
|
|
||||||
fields=self.fields,
|
|
||||||
filters=self.filters,
|
|
||||||
or_filters=self.or_filters,
|
|
||||||
limit_page_length=page_length,
|
|
||||||
limit_start=start,
|
|
||||||
order_by="ranking desc",
|
|
||||||
)
|
|
||||||
|
|
||||||
return items, count
|
|
||||||
|
|
||||||
def query_items_with_attributes(self, attributes, start=0):
|
|
||||||
"""Build a query to fetch Website Items based on field & attribute filters."""
|
|
||||||
item_codes = []
|
|
||||||
|
|
||||||
for attribute, values in attributes.items():
|
|
||||||
if not isinstance(values, list):
|
|
||||||
values = [values]
|
|
||||||
|
|
||||||
# get items that have selected attribute & value
|
|
||||||
item_code_list = frappe.db.get_all(
|
|
||||||
"Item",
|
|
||||||
fields=["item_code"],
|
|
||||||
filters=[
|
|
||||||
["published_in_website", "=", 1],
|
|
||||||
["Item Variant Attribute", "attribute", "=", attribute],
|
|
||||||
["Item Variant Attribute", "attribute_value", "in", values],
|
|
||||||
],
|
|
||||||
)
|
|
||||||
item_codes.append({x.item_code for x in item_code_list})
|
|
||||||
|
|
||||||
if item_codes:
|
|
||||||
item_codes = list(set.intersection(*item_codes))
|
|
||||||
self.filters.append(["item_code", "in", item_codes])
|
|
||||||
|
|
||||||
items, count = self.query_items(start=start)
|
|
||||||
|
|
||||||
return items, count
|
|
||||||
|
|
||||||
def build_fields_filters(self, filters):
|
|
||||||
"""Build filters for field values
|
|
||||||
|
|
||||||
Args:
|
|
||||||
filters (dict): Filters
|
|
||||||
"""
|
|
||||||
for field, values in filters.items():
|
|
||||||
if not values or field == "discount":
|
|
||||||
continue
|
|
||||||
|
|
||||||
# handle multiselect fields in filter addition
|
|
||||||
meta = frappe.get_meta("Website Item", cached=True)
|
|
||||||
df = meta.get_field(field)
|
|
||||||
if df.fieldtype == "Table MultiSelect":
|
|
||||||
child_doctype = df.options
|
|
||||||
child_meta = frappe.get_meta(child_doctype, cached=True)
|
|
||||||
fields = child_meta.get("fields")
|
|
||||||
if fields:
|
|
||||||
self.filters.append([child_doctype, fields[0].fieldname, "IN", values])
|
|
||||||
elif isinstance(values, list):
|
|
||||||
# If value is a list use `IN` query
|
|
||||||
self.filters.append([field, "in", values])
|
|
||||||
else:
|
|
||||||
# `=` will be faster than `IN` for most cases
|
|
||||||
self.filters.append([field, "=", values])
|
|
||||||
|
|
||||||
def build_item_group_filters(self, item_group):
|
|
||||||
"Add filters for Item group page and include Website Item Groups."
|
|
||||||
from erpnext.setup.doctype.item_group.item_group import get_child_groups_for_website
|
|
||||||
|
|
||||||
item_group_filters = []
|
|
||||||
|
|
||||||
item_group_filters.append(["Website Item", "item_group", "=", item_group])
|
|
||||||
# Consider Website Item Groups
|
|
||||||
item_group_filters.append(["Website Item Group", "item_group", "=", item_group])
|
|
||||||
|
|
||||||
if frappe.db.get_value("Item Group", item_group, "include_descendants"):
|
|
||||||
# include child item group's items as well
|
|
||||||
# eg. Group Node A, will show items of child 1 and child 2 as well
|
|
||||||
# on it's web page
|
|
||||||
include_groups = get_child_groups_for_website(item_group, include_self=True)
|
|
||||||
include_groups = [x.name for x in include_groups]
|
|
||||||
item_group_filters.append(["Website Item", "item_group", "in", include_groups])
|
|
||||||
|
|
||||||
self.or_filters.extend(item_group_filters)
|
|
||||||
|
|
||||||
def build_search_filters(self, search_term):
|
|
||||||
"""Query search term in specified fields
|
|
||||||
|
|
||||||
Args:
|
|
||||||
search_term (str): Search candidate
|
|
||||||
"""
|
|
||||||
# Default fields to search from
|
|
||||||
default_fields = {"item_code", "item_name", "web_long_description", "item_group"}
|
|
||||||
|
|
||||||
# Get meta search fields
|
|
||||||
meta = frappe.get_meta("Website Item")
|
|
||||||
meta_fields = set(meta.get_search_fields())
|
|
||||||
|
|
||||||
# Join the meta fields and default fields set
|
|
||||||
search_fields = default_fields.union(meta_fields)
|
|
||||||
if frappe.db.count("Website Item", cache=True) > 50000:
|
|
||||||
search_fields.discard("web_long_description")
|
|
||||||
|
|
||||||
# Build or filters for query
|
|
||||||
search = "%{}%".format(search_term)
|
|
||||||
for field in search_fields:
|
|
||||||
self.or_filters.append([field, "like", search])
|
|
||||||
|
|
||||||
def add_display_details(self, result, discount_list, cart_items):
|
|
||||||
"""Add price and availability details in result."""
|
|
||||||
for item in result:
|
|
||||||
product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get(
|
|
||||||
"product_info"
|
|
||||||
)
|
|
||||||
|
|
||||||
if product_info and product_info["price"]:
|
|
||||||
# update/mutate item and discount_list objects
|
|
||||||
self.get_price_discount_info(item, product_info["price"], discount_list)
|
|
||||||
|
|
||||||
if self.settings.show_stock_availability:
|
|
||||||
self.get_stock_availability(item)
|
|
||||||
|
|
||||||
item.in_cart = item.item_code in cart_items
|
|
||||||
|
|
||||||
item.wished = False
|
|
||||||
if frappe.db.exists(
|
|
||||||
"Wishlist Item", {"item_code": item.item_code, "parent": frappe.session.user}
|
|
||||||
):
|
|
||||||
item.wished = True
|
|
||||||
|
|
||||||
return result, discount_list
|
|
||||||
|
|
||||||
def get_price_discount_info(self, item, price_object, discount_list):
|
|
||||||
"""Modify item object and add price details."""
|
|
||||||
fields = ["formatted_mrp", "formatted_price", "price_list_rate"]
|
|
||||||
for field in fields:
|
|
||||||
item[field] = price_object.get(field)
|
|
||||||
|
|
||||||
if price_object.get("discount_percent"):
|
|
||||||
item.discount_percent = flt(price_object.discount_percent)
|
|
||||||
discount_list.append(price_object.discount_percent)
|
|
||||||
|
|
||||||
if item.formatted_mrp:
|
|
||||||
item.discount = price_object.get("formatted_discount_percent") or price_object.get(
|
|
||||||
"formatted_discount_rate"
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_stock_availability(self, item):
|
|
||||||
from erpnext.templates.pages.wishlist import (
|
|
||||||
get_stock_availability as get_stock_availability_from_template,
|
|
||||||
)
|
|
||||||
|
|
||||||
"""Modify item object and add stock details."""
|
|
||||||
item.in_stock = False
|
|
||||||
warehouse = item.get("website_warehouse")
|
|
||||||
is_stock_item = frappe.get_cached_value("Item", item.item_code, "is_stock_item")
|
|
||||||
|
|
||||||
if item.get("on_backorder"):
|
|
||||||
return
|
|
||||||
|
|
||||||
if not is_stock_item:
|
|
||||||
if warehouse:
|
|
||||||
# product bundle case
|
|
||||||
item.in_stock = get_non_stock_item_status(item.item_code, "website_warehouse")
|
|
||||||
else:
|
|
||||||
item.in_stock = True
|
|
||||||
elif warehouse:
|
|
||||||
item.in_stock = get_stock_availability_from_template(item.item_code, warehouse)
|
|
||||||
|
|
||||||
def get_cart_items(self):
|
|
||||||
customer = get_customer(silent=True)
|
|
||||||
if customer:
|
|
||||||
quotation = frappe.get_all(
|
|
||||||
"Quotation",
|
|
||||||
fields=["name"],
|
|
||||||
filters={
|
|
||||||
"party_name": customer,
|
|
||||||
"contact_email": frappe.session.user,
|
|
||||||
"order_type": "Shopping Cart",
|
|
||||||
"docstatus": 0,
|
|
||||||
},
|
|
||||||
order_by="modified desc",
|
|
||||||
limit_page_length=1,
|
|
||||||
)
|
|
||||||
if quotation:
|
|
||||||
items = frappe.get_all(
|
|
||||||
"Quotation Item", fields=["item_code"], filters={"parent": quotation[0].get("name")}
|
|
||||||
)
|
|
||||||
items = [row.item_code for row in items]
|
|
||||||
return items
|
|
||||||
|
|
||||||
return []
|
|
||||||
|
|
||||||
def filter_results_by_discount(self, fields, result):
|
|
||||||
if fields and fields.get("discount"):
|
|
||||||
discount_percent = frappe.utils.flt(fields["discount"][0])
|
|
||||||
result = [
|
|
||||||
row
|
|
||||||
for row in result
|
|
||||||
if row.get("discount_percent") and row.discount_percent <= discount_percent
|
|
||||||
]
|
|
||||||
|
|
||||||
if self.filter_with_discount:
|
|
||||||
# no limit was added to results while querying
|
|
||||||
# slice results manually
|
|
||||||
result[: self.page_length]
|
|
||||||
|
|
||||||
return result
|
|
@ -1,170 +0,0 @@
|
|||||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
|
||||||
# For license information, please see license.txt
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import frappe
|
|
||||||
|
|
||||||
from erpnext.e_commerce.api import get_product_filter_data
|
|
||||||
from erpnext.e_commerce.doctype.website_item.test_website_item import create_regular_web_item
|
|
||||||
|
|
||||||
test_dependencies = ["Item", "Item Group"]
|
|
||||||
|
|
||||||
|
|
||||||
class TestItemGroupProductDataEngine(unittest.TestCase):
|
|
||||||
"Test Products & Sub-Category Querying for Product Listing on Item Group Page."
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
item_codes = [
|
|
||||||
("Test Mobile A", "_Test Item Group B"),
|
|
||||||
("Test Mobile B", "_Test Item Group B"),
|
|
||||||
("Test Mobile C", "_Test Item Group B - 1"),
|
|
||||||
("Test Mobile D", "_Test Item Group B - 1"),
|
|
||||||
("Test Mobile E", "_Test Item Group B - 2"),
|
|
||||||
]
|
|
||||||
for item in item_codes:
|
|
||||||
item_code = item[0]
|
|
||||||
item_args = {"item_group": item[1]}
|
|
||||||
if not frappe.db.exists("Website Item", {"item_code": item_code}):
|
|
||||||
create_regular_web_item(item_code, item_args=item_args)
|
|
||||||
|
|
||||||
frappe.db.set_value("Item Group", "_Test Item Group B - 1", "show_in_website", 1)
|
|
||||||
frappe.db.set_value("Item Group", "_Test Item Group B - 2", "show_in_website", 1)
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
frappe.db.rollback()
|
|
||||||
|
|
||||||
def test_product_listing_in_item_group(self):
|
|
||||||
"Test if only products belonging to the Item Group are fetched."
|
|
||||||
result = get_product_filter_data(
|
|
||||||
query_args={
|
|
||||||
"field_filters": {},
|
|
||||||
"attribute_filters": {},
|
|
||||||
"start": 0,
|
|
||||||
"item_group": "_Test Item Group B",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
items = result.get("items")
|
|
||||||
item_codes = [item.get("item_code") for item in items]
|
|
||||||
|
|
||||||
self.assertEqual(len(items), 2)
|
|
||||||
self.assertIn("Test Mobile A", item_codes)
|
|
||||||
self.assertNotIn("Test Mobile C", item_codes)
|
|
||||||
|
|
||||||
def test_products_in_multiple_item_groups(self):
|
|
||||||
"""Test if product is visible on multiple item group pages barring its own."""
|
|
||||||
website_item = frappe.get_doc("Website Item", {"item_code": "Test Mobile E"})
|
|
||||||
|
|
||||||
# show item belonging to '_Test Item Group B - 2' in '_Test Item Group B - 1' as well
|
|
||||||
website_item.append("website_item_groups", {"item_group": "_Test Item Group B - 1"})
|
|
||||||
website_item.save()
|
|
||||||
|
|
||||||
result = get_product_filter_data(
|
|
||||||
query_args={
|
|
||||||
"field_filters": {},
|
|
||||||
"attribute_filters": {},
|
|
||||||
"start": 0,
|
|
||||||
"item_group": "_Test Item Group B - 1",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
items = result.get("items")
|
|
||||||
item_codes = [item.get("item_code") for item in items]
|
|
||||||
|
|
||||||
self.assertEqual(len(items), 3)
|
|
||||||
self.assertIn("Test Mobile E", item_codes) # visible in other item groups
|
|
||||||
self.assertIn("Test Mobile C", item_codes)
|
|
||||||
self.assertIn("Test Mobile D", item_codes)
|
|
||||||
|
|
||||||
result = get_product_filter_data(
|
|
||||||
query_args={
|
|
||||||
"field_filters": {},
|
|
||||||
"attribute_filters": {},
|
|
||||||
"start": 0,
|
|
||||||
"item_group": "_Test Item Group B - 2",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
items = result.get("items")
|
|
||||||
|
|
||||||
self.assertEqual(len(items), 1)
|
|
||||||
self.assertEqual(items[0].get("item_code"), "Test Mobile E") # visible in own item group
|
|
||||||
|
|
||||||
def test_item_group_with_sub_groups(self):
|
|
||||||
"Test Valid Sub Item Groups in Item Group Page."
|
|
||||||
frappe.db.set_value("Item Group", "_Test Item Group B - 2", "show_in_website", 0)
|
|
||||||
|
|
||||||
result = get_product_filter_data(
|
|
||||||
query_args={
|
|
||||||
"field_filters": {},
|
|
||||||
"attribute_filters": {},
|
|
||||||
"start": 0,
|
|
||||||
"item_group": "_Test Item Group B",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertTrue(bool(result.get("sub_categories")))
|
|
||||||
|
|
||||||
child_groups = [d.name for d in result.get("sub_categories")]
|
|
||||||
# check if child group is fetched if shown in website
|
|
||||||
self.assertIn("_Test Item Group B - 1", child_groups)
|
|
||||||
|
|
||||||
frappe.db.set_value("Item Group", "_Test Item Group B - 2", "show_in_website", 1)
|
|
||||||
result = get_product_filter_data(
|
|
||||||
query_args={
|
|
||||||
"field_filters": {},
|
|
||||||
"attribute_filters": {},
|
|
||||||
"start": 0,
|
|
||||||
"item_group": "_Test Item Group B",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
child_groups = [d.name for d in result.get("sub_categories")]
|
|
||||||
|
|
||||||
# check if child group is fetched if shown in website
|
|
||||||
self.assertIn("_Test Item Group B - 1", child_groups)
|
|
||||||
self.assertIn("_Test Item Group B - 2", child_groups)
|
|
||||||
|
|
||||||
def test_item_group_page_with_descendants_included(self):
|
|
||||||
"""
|
|
||||||
Test if 'include_descendants' pulls Items belonging to descendant Item Groups (Level 2 & 3).
|
|
||||||
> _Test Item Group B [Level 1]
|
|
||||||
> _Test Item Group B - 1 [Level 2]
|
|
||||||
> _Test Item Group B - 1 - 1 [Level 3]
|
|
||||||
"""
|
|
||||||
frappe.get_doc(
|
|
||||||
{ # create Level 3 nested child group
|
|
||||||
"doctype": "Item Group",
|
|
||||||
"is_group": 1,
|
|
||||||
"item_group_name": "_Test Item Group B - 1 - 1",
|
|
||||||
"parent_item_group": "_Test Item Group B - 1",
|
|
||||||
}
|
|
||||||
).insert()
|
|
||||||
|
|
||||||
create_regular_web_item( # create an item belonging to level 3 item group
|
|
||||||
"Test Mobile F", item_args={"item_group": "_Test Item Group B - 1 - 1"}
|
|
||||||
)
|
|
||||||
|
|
||||||
frappe.db.set_value("Item Group", "_Test Item Group B - 1 - 1", "show_in_website", 1)
|
|
||||||
|
|
||||||
# enable 'include descendants' in Level 1
|
|
||||||
frappe.db.set_value("Item Group", "_Test Item Group B", "include_descendants", 1)
|
|
||||||
|
|
||||||
result = get_product_filter_data(
|
|
||||||
query_args={
|
|
||||||
"field_filters": {},
|
|
||||||
"attribute_filters": {},
|
|
||||||
"start": 0,
|
|
||||||
"item_group": "_Test Item Group B",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
items = result.get("items")
|
|
||||||
item_codes = [item.get("item_code") for item in items]
|
|
||||||
|
|
||||||
# check if all sub groups' items are pulled
|
|
||||||
self.assertEqual(len(items), 6)
|
|
||||||
self.assertIn("Test Mobile A", item_codes)
|
|
||||||
self.assertIn("Test Mobile C", item_codes)
|
|
||||||
self.assertIn("Test Mobile E", item_codes)
|
|
||||||
self.assertIn("Test Mobile F", item_codes)
|
|
@ -1,348 +0,0 @@
|
|||||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
|
||||||
# For license information, please see license.txt
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import frappe
|
|
||||||
|
|
||||||
from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import (
|
|
||||||
setup_e_commerce_settings,
|
|
||||||
)
|
|
||||||
from erpnext.e_commerce.doctype.website_item.test_website_item import create_regular_web_item
|
|
||||||
from erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder
|
|
||||||
from erpnext.e_commerce.product_data_engine.query import ProductQuery
|
|
||||||
|
|
||||||
test_dependencies = ["Item", "Item Group"]
|
|
||||||
|
|
||||||
|
|
||||||
class TestProductDataEngine(unittest.TestCase):
|
|
||||||
"Test Products Querying and Filters for Product Listing."
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def setUpClass(cls):
|
|
||||||
item_codes = [
|
|
||||||
("Test 11I Laptop", "Products"), # rank 1
|
|
||||||
("Test 12I Laptop", "Products"), # rank 2
|
|
||||||
("Test 13I Laptop", "Products"), # rank 3
|
|
||||||
("Test 14I Laptop", "Raw Material"), # rank 4
|
|
||||||
("Test 15I Laptop", "Raw Material"), # rank 5
|
|
||||||
("Test 16I Laptop", "Raw Material"), # rank 6
|
|
||||||
("Test 17I Laptop", "Products"), # rank 7
|
|
||||||
]
|
|
||||||
for index, item in enumerate(item_codes, start=1):
|
|
||||||
item_code = item[0]
|
|
||||||
item_args = {"item_group": item[1]}
|
|
||||||
web_args = {"ranking": index}
|
|
||||||
if not frappe.db.exists("Website Item", {"item_code": item_code}):
|
|
||||||
create_regular_web_item(item_code, item_args=item_args, web_args=web_args)
|
|
||||||
|
|
||||||
setup_e_commerce_settings(
|
|
||||||
{
|
|
||||||
"products_per_page": 4,
|
|
||||||
"enable_field_filters": 1,
|
|
||||||
"filter_fields": [{"fieldname": "item_group"}],
|
|
||||||
"enable_attribute_filters": 1,
|
|
||||||
"filter_attributes": [{"attribute": "Test Size"}],
|
|
||||||
"company": "_Test Company",
|
|
||||||
"enabled": 1,
|
|
||||||
"default_customer_group": "_Test Customer Group",
|
|
||||||
"price_list": "_Test Price List India",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
frappe.local.shopping_cart_settings = None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def tearDownClass(cls):
|
|
||||||
frappe.db.rollback()
|
|
||||||
|
|
||||||
def test_product_list_ordering_and_paging(self):
|
|
||||||
"Test if website items appear by ranking on different pages."
|
|
||||||
engine = ProductQuery()
|
|
||||||
result = engine.query(attributes={}, fields={}, search_term=None, start=0, item_group=None)
|
|
||||||
items = result.get("items")
|
|
||||||
|
|
||||||
self.assertIsNotNone(items)
|
|
||||||
self.assertEqual(len(items), 4)
|
|
||||||
self.assertGreater(result.get("items_count"), 4)
|
|
||||||
|
|
||||||
# check if items appear as per ranking set in setUpClass
|
|
||||||
self.assertEqual(items[0].get("item_code"), "Test 17I Laptop")
|
|
||||||
self.assertEqual(items[1].get("item_code"), "Test 16I Laptop")
|
|
||||||
self.assertEqual(items[2].get("item_code"), "Test 15I Laptop")
|
|
||||||
self.assertEqual(items[3].get("item_code"), "Test 14I Laptop")
|
|
||||||
|
|
||||||
# check next page
|
|
||||||
result = engine.query(attributes={}, fields={}, search_term=None, start=4, item_group=None)
|
|
||||||
items = result.get("items")
|
|
||||||
|
|
||||||
# check if items appear as per ranking set in setUpClass on next page
|
|
||||||
self.assertEqual(items[0].get("item_code"), "Test 13I Laptop")
|
|
||||||
self.assertEqual(items[1].get("item_code"), "Test 12I Laptop")
|
|
||||||
self.assertEqual(items[2].get("item_code"), "Test 11I Laptop")
|
|
||||||
|
|
||||||
def test_change_product_ranking(self):
|
|
||||||
"Test if item on second page appear on first if ranking is changed."
|
|
||||||
item_code = "Test 12I Laptop"
|
|
||||||
old_ranking = frappe.db.get_value("Website Item", {"item_code": item_code}, "ranking")
|
|
||||||
|
|
||||||
# low rank, appears on second page
|
|
||||||
self.assertEqual(old_ranking, 2)
|
|
||||||
|
|
||||||
# set ranking as highest rank
|
|
||||||
frappe.db.set_value("Website Item", {"item_code": item_code}, "ranking", 10)
|
|
||||||
|
|
||||||
engine = ProductQuery()
|
|
||||||
result = engine.query(attributes={}, fields={}, search_term=None, start=0, item_group=None)
|
|
||||||
items = result.get("items")
|
|
||||||
|
|
||||||
# check if item is the first item on the first page
|
|
||||||
self.assertEqual(items[0].get("item_code"), item_code)
|
|
||||||
self.assertEqual(items[1].get("item_code"), "Test 17I Laptop")
|
|
||||||
|
|
||||||
# tear down
|
|
||||||
frappe.db.set_value("Website Item", {"item_code": item_code}, "ranking", old_ranking)
|
|
||||||
|
|
||||||
def test_product_list_field_filter_builder(self):
|
|
||||||
"Test if field filters are fetched correctly."
|
|
||||||
frappe.db.set_value("Item Group", "Raw Material", "show_in_website", 0)
|
|
||||||
|
|
||||||
filter_engine = ProductFiltersBuilder()
|
|
||||||
field_filters = filter_engine.get_field_filters()
|
|
||||||
|
|
||||||
# Web Items belonging to 'Products' and 'Raw Material' are available
|
|
||||||
# but only 'Products' has 'show_in_website' enabled
|
|
||||||
item_group_filters = field_filters[0]
|
|
||||||
docfield = item_group_filters[0]
|
|
||||||
valid_item_groups = item_group_filters[1]
|
|
||||||
|
|
||||||
self.assertEqual(docfield.options, "Item Group")
|
|
||||||
self.assertIn("Products", valid_item_groups)
|
|
||||||
self.assertNotIn("Raw Material", valid_item_groups)
|
|
||||||
|
|
||||||
frappe.db.set_value("Item Group", "Raw Material", "show_in_website", 1)
|
|
||||||
field_filters = filter_engine.get_field_filters()
|
|
||||||
|
|
||||||
#'Products' and 'Raw Materials' both have 'show_in_website' enabled
|
|
||||||
item_group_filters = field_filters[0]
|
|
||||||
docfield = item_group_filters[0]
|
|
||||||
valid_item_groups = item_group_filters[1]
|
|
||||||
|
|
||||||
self.assertEqual(docfield.options, "Item Group")
|
|
||||||
self.assertIn("Products", valid_item_groups)
|
|
||||||
self.assertIn("Raw Material", valid_item_groups)
|
|
||||||
|
|
||||||
def test_product_list_with_field_filter(self):
|
|
||||||
"Test if field filters are applied correctly."
|
|
||||||
field_filters = {"item_group": "Raw Material"}
|
|
||||||
|
|
||||||
engine = ProductQuery()
|
|
||||||
result = engine.query(
|
|
||||||
attributes={}, fields=field_filters, search_term=None, start=0, item_group=None
|
|
||||||
)
|
|
||||||
items = result.get("items")
|
|
||||||
|
|
||||||
# check if only 'Raw Material' are fetched in the right order
|
|
||||||
self.assertEqual(len(items), 3)
|
|
||||||
self.assertEqual(items[0].get("item_code"), "Test 16I Laptop")
|
|
||||||
self.assertEqual(items[1].get("item_code"), "Test 15I Laptop")
|
|
||||||
|
|
||||||
# def test_product_list_with_field_filter_table_multiselect(self):
|
|
||||||
# TODO
|
|
||||||
# pass
|
|
||||||
|
|
||||||
def test_product_list_attribute_filter_builder(self):
|
|
||||||
"Test if attribute filters are fetched correctly."
|
|
||||||
create_variant_web_item()
|
|
||||||
|
|
||||||
filter_engine = ProductFiltersBuilder()
|
|
||||||
attribute_filter = filter_engine.get_attribute_filters()[0]
|
|
||||||
attribute_values = attribute_filter.item_attribute_values
|
|
||||||
|
|
||||||
self.assertEqual(attribute_filter.name, "Test Size")
|
|
||||||
self.assertGreater(len(attribute_values), 0)
|
|
||||||
self.assertIn("Large", attribute_values)
|
|
||||||
|
|
||||||
def test_product_list_with_attribute_filter(self):
|
|
||||||
"Test if attribute filters are applied correctly."
|
|
||||||
create_variant_web_item()
|
|
||||||
|
|
||||||
attribute_filters = {"Test Size": ["Large"]}
|
|
||||||
engine = ProductQuery()
|
|
||||||
result = engine.query(
|
|
||||||
attributes=attribute_filters, fields={}, search_term=None, start=0, item_group=None
|
|
||||||
)
|
|
||||||
items = result.get("items")
|
|
||||||
|
|
||||||
# check if only items with Test Size 'Large' are fetched
|
|
||||||
self.assertEqual(len(items), 1)
|
|
||||||
self.assertEqual(items[0].get("item_code"), "Test Web Item-L")
|
|
||||||
|
|
||||||
def test_product_list_discount_filter_builder(self):
|
|
||||||
"Test if discount filters are fetched correctly."
|
|
||||||
from erpnext.e_commerce.doctype.website_item.test_website_item import (
|
|
||||||
make_web_item_price,
|
|
||||||
make_web_pricing_rule,
|
|
||||||
)
|
|
||||||
|
|
||||||
item_code = "Test 12I Laptop"
|
|
||||||
make_web_item_price(item_code=item_code)
|
|
||||||
make_web_pricing_rule(title=f"Test Pricing Rule for {item_code}", item_code=item_code, selling=1)
|
|
||||||
|
|
||||||
setup_e_commerce_settings({"show_price": 1})
|
|
||||||
frappe.local.shopping_cart_settings = None
|
|
||||||
|
|
||||||
engine = ProductQuery()
|
|
||||||
result = engine.query(attributes={}, fields={}, search_term=None, start=4, item_group=None)
|
|
||||||
self.assertTrue(bool(result.get("discounts")))
|
|
||||||
|
|
||||||
filter_engine = ProductFiltersBuilder()
|
|
||||||
discount_filters = filter_engine.get_discount_filters(result["discounts"])
|
|
||||||
|
|
||||||
self.assertEqual(len(discount_filters[0]), 2)
|
|
||||||
self.assertEqual(discount_filters[0][0], 10)
|
|
||||||
self.assertEqual(discount_filters[0][1], "10% and below")
|
|
||||||
|
|
||||||
def test_product_list_with_discount_filters(self):
|
|
||||||
"Test if discount filters are applied correctly."
|
|
||||||
from erpnext.e_commerce.doctype.website_item.test_website_item import (
|
|
||||||
make_web_item_price,
|
|
||||||
make_web_pricing_rule,
|
|
||||||
)
|
|
||||||
|
|
||||||
field_filters = {"discount": [10]}
|
|
||||||
|
|
||||||
make_web_item_price(item_code="Test 12I Laptop")
|
|
||||||
make_web_pricing_rule(
|
|
||||||
title="Test Pricing Rule for Test 12I Laptop", # 10% discount
|
|
||||||
item_code="Test 12I Laptop",
|
|
||||||
selling=1,
|
|
||||||
)
|
|
||||||
make_web_item_price(item_code="Test 13I Laptop")
|
|
||||||
make_web_pricing_rule(
|
|
||||||
title="Test Pricing Rule for Test 13I Laptop", # 15% discount
|
|
||||||
item_code="Test 13I Laptop",
|
|
||||||
discount_percentage=15,
|
|
||||||
selling=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
setup_e_commerce_settings({"show_price": 1})
|
|
||||||
frappe.local.shopping_cart_settings = None
|
|
||||||
|
|
||||||
engine = ProductQuery()
|
|
||||||
result = engine.query(
|
|
||||||
attributes={}, fields=field_filters, search_term=None, start=0, item_group=None
|
|
||||||
)
|
|
||||||
items = result.get("items")
|
|
||||||
|
|
||||||
# check if only product with 10% and below discount are fetched
|
|
||||||
self.assertEqual(len(items), 1)
|
|
||||||
self.assertEqual(items[0].get("item_code"), "Test 12I Laptop")
|
|
||||||
|
|
||||||
def test_product_list_with_api(self):
|
|
||||||
"Test products listing using API."
|
|
||||||
from erpnext.e_commerce.api import get_product_filter_data
|
|
||||||
|
|
||||||
create_variant_web_item()
|
|
||||||
|
|
||||||
result = get_product_filter_data(
|
|
||||||
query_args={
|
|
||||||
"field_filters": {"item_group": "Products"},
|
|
||||||
"attribute_filters": {"Test Size": ["Large"]},
|
|
||||||
"start": 0,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
items = result.get("items")
|
|
||||||
|
|
||||||
self.assertEqual(len(items), 1)
|
|
||||||
self.assertEqual(items[0].get("item_code"), "Test Web Item-L")
|
|
||||||
|
|
||||||
def test_product_list_with_variants(self):
|
|
||||||
"Test if variants are hideen on hiding variants in settings."
|
|
||||||
create_variant_web_item()
|
|
||||||
|
|
||||||
setup_e_commerce_settings({"enable_attribute_filters": 0, "hide_variants": 1})
|
|
||||||
frappe.local.shopping_cart_settings = None
|
|
||||||
|
|
||||||
attribute_filters = {"Test Size": ["Large"]}
|
|
||||||
engine = ProductQuery()
|
|
||||||
result = engine.query(
|
|
||||||
attributes=attribute_filters, fields={}, search_term=None, start=0, item_group=None
|
|
||||||
)
|
|
||||||
items = result.get("items")
|
|
||||||
|
|
||||||
# check if any variants are fetched even though published variant exists
|
|
||||||
self.assertEqual(len(items), 0)
|
|
||||||
|
|
||||||
# tear down
|
|
||||||
setup_e_commerce_settings({"enable_attribute_filters": 1, "hide_variants": 0})
|
|
||||||
|
|
||||||
def test_custom_field_as_filter(self):
|
|
||||||
"Test if custom field functions as filter correctly."
|
|
||||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
|
||||||
|
|
||||||
create_custom_field(
|
|
||||||
"Website Item",
|
|
||||||
dict(
|
|
||||||
owner="Administrator",
|
|
||||||
fieldname="supplier",
|
|
||||||
label="Supplier",
|
|
||||||
fieldtype="Link",
|
|
||||||
options="Supplier",
|
|
||||||
insert_after="on_backorder",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
frappe.db.set_value(
|
|
||||||
"Website Item", {"item_code": "Test 11I Laptop"}, "supplier", "_Test Supplier"
|
|
||||||
)
|
|
||||||
frappe.db.set_value(
|
|
||||||
"Website Item", {"item_code": "Test 12I Laptop"}, "supplier", "_Test Supplier 1"
|
|
||||||
)
|
|
||||||
|
|
||||||
settings = frappe.get_doc("E Commerce Settings")
|
|
||||||
settings.append("filter_fields", {"fieldname": "supplier"})
|
|
||||||
settings.save()
|
|
||||||
|
|
||||||
filter_engine = ProductFiltersBuilder()
|
|
||||||
field_filters = filter_engine.get_field_filters()
|
|
||||||
custom_filter = field_filters[1]
|
|
||||||
filter_values = custom_filter[1]
|
|
||||||
|
|
||||||
self.assertEqual(custom_filter[0].options, "Supplier")
|
|
||||||
self.assertEqual(len(filter_values), 2)
|
|
||||||
self.assertIn("_Test Supplier", filter_values)
|
|
||||||
|
|
||||||
# test if custom filter works in query
|
|
||||||
field_filters = {"supplier": "_Test Supplier 1"}
|
|
||||||
engine = ProductQuery()
|
|
||||||
result = engine.query(
|
|
||||||
attributes={}, fields=field_filters, search_term=None, start=0, item_group=None
|
|
||||||
)
|
|
||||||
items = result.get("items")
|
|
||||||
|
|
||||||
# check if only 'Raw Material' are fetched in the right order
|
|
||||||
self.assertEqual(len(items), 1)
|
|
||||||
self.assertEqual(items[0].get("item_code"), "Test 12I Laptop")
|
|
||||||
|
|
||||||
|
|
||||||
def create_variant_web_item():
|
|
||||||
"Create Variant and Template Website Items."
|
|
||||||
from erpnext.controllers.item_variant import create_variant
|
|
||||||
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
|
|
||||||
from erpnext.stock.doctype.item.test_item import make_item
|
|
||||||
|
|
||||||
make_item(
|
|
||||||
"Test Web Item",
|
|
||||||
{
|
|
||||||
"has_variant": 1,
|
|
||||||
"variant_based_on": "Item Attribute",
|
|
||||||
"attributes": [{"attribute": "Test Size"}],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if not frappe.db.exists("Item", "Test Web Item-L"):
|
|
||||||
variant = create_variant("Test Web Item", {"Test Size": "Large"})
|
|
||||||
variant.save()
|
|
||||||
|
|
||||||
if not frappe.db.exists("Website Item", {"variant_of": "Test Web Item"}):
|
|
||||||
make_website_item(variant, save=True)
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user