Merge branch 'develop' into leave-opening-balance

This commit is contained in:
Rucha Mahabal
2022-02-28 10:03:38 +05:30
committed by GitHub
535 changed files with 22461 additions and 8603 deletions

View File

@@ -28,6 +28,7 @@ ignore =
B007, B007,
B950, B950,
W191, W191,
E124, # closing bracket, irritating while writing QB code
max-line-length = 200 max-line-length = 200
exclude=.github/helper/semgrep_rules exclude=.github/helper/semgrep_rules

View File

@@ -8,7 +8,10 @@ sudo apt-get install redis-server libcups2-dev
pip install frappe-bench pip install frappe-bench
git clone https://github.com/frappe/frappe --branch "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}" --depth 1 frappeuser=${FRAPPE_USER:-"frappe"}
frappebranch=${FRAPPE_BRANCH:-${GITHUB_BASE_REF:-${GITHUB_REF##*/}}}
git clone "https://github.com/${frappeuser}/frappe" --branch "${frappebranch}" --depth 1
bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench
mkdir ~/frappe-bench/sites/test_site mkdir ~/frappe-bench/sites/test_site
@@ -37,10 +40,14 @@ if [ "$DB" == "postgres" ];then
echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE USER test_frappe WITH PASSWORD 'test_frappe'" -U postgres; echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE USER test_frappe WITH PASSWORD 'test_frappe'" -U postgres;
fi fi
wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz
tar -xf /tmp/wkhtmltox.tar.xz -C /tmp install_whktml() {
sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz
sudo chmod o+x /usr/local/bin/wkhtmltopdf tar -xf /tmp/wkhtmltox.tar.xz -C /tmp
sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf
sudo chmod o+x /usr/local/bin/wkhtmltopdf
}
install_whktml &
cd ~/frappe-bench || exit cd ~/frappe-bench || exit
@@ -54,5 +61,5 @@ bench get-app erpnext "${GITHUB_WORKSPACE}"
if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi
bench start &> bench_run_logs.txt & bench start &> bench_run_logs.txt &
CI=Yes bench build --app frappe &
bench --site test_site reinstall --yes bench --site test_site reinstall --yes
bench build --app frappe

1
.github/stale.yml vendored
View File

@@ -30,6 +30,7 @@ issues:
exemptLabels: exemptLabels:
- valid - valid
- to-validate - to-validate
- QA
markComment: > markComment: >
This issue has been automatically marked as inactive because it has not had This issue has been automatically marked as inactive because it has not had
recent activity and it wasn't validated by maintainer team. It will be recent activity and it wasn't validated by maintainer team. It will be

View File

@@ -6,12 +6,23 @@ on:
- '**.js' - '**.js'
- '**.md' - '**.md'
- '**.html' - '**.html'
workflow_dispatch:
push: push:
branches: [ develop ] branches: [ develop ]
paths-ignore: paths-ignore:
- '**.js' - '**.js'
- '**.md' - '**.md'
workflow_dispatch:
inputs:
user:
description: 'user'
required: true
default: 'frappe'
type: string
branch:
description: 'Branch name'
default: 'develop'
required: false
type: string
concurrency: concurrency:
group: server-mariadb-develop-${{ github.event.number }} group: server-mariadb-develop-${{ github.event.number }}
@@ -95,6 +106,8 @@ jobs:
env: env:
DB: mariadb DB: mariadb
TYPE: server TYPE: server
FRAPPE_USER: ${{ github.event.inputs.user }}
FRAPPE_BRANCH: ${{ github.event.inputs.branch }}
- name: Run Tests - name: Run Tests
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --use-orchestrator --with-coverage run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --use-orchestrator --with-coverage

View File

@@ -14,9 +14,39 @@ pull_request_rules:
close: close:
comment: comment:
message: | message: |
@{{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: backport to develop
conditions:
- label="backport develop"
actions:
backport:
branches:
- develop
assignees:
- "{{ author }}"
- name: backport to version-14-hotfix
conditions:
- label="backport version-14-hotfix"
actions:
backport:
branches:
- version-14-hotfix
assignees:
- "{{ author }}"
- name: backport to version-14-pre-release
conditions:
- label="backport version-14-pre-release"
actions:
backport:
branches:
- version-14-pre-release
assignees:
- "{{ author }}"
- name: backport to version-13-hotfix - name: backport to version-13-hotfix
conditions: conditions:
- label="backport version-13-hotfix" - label="backport version-13-hotfix"
@@ -55,4 +85,4 @@ pull_request_rules:
branches: branches:
- version-12-pre-release - version-12-pre-release
assignees: assignees:
- "{{ author }}" - "{{ author }}"

View File

@@ -0,0 +1,44 @@
describe("Bulk Transaction Processing", () => {
before(() => {
cy.login();
cy.visit("/app/website");
});
it("Creates To Sales Order", () => {
cy.visit("/app/sales-order");
cy.url().should("include", "/sales-order");
cy.window()
.its("frappe.csrf_token")
.then((csrf_token) => {
return cy
.request({
url: "/api/method/erpnext.tests.ui_test_bulk_transaction_processing.create_records",
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"X-Frappe-CSRF-Token": csrf_token,
},
timeout: 60000,
})
.then((res) => {
expect(res.status).eq(200);
});
});
cy.wait(5000);
cy.get(
".list-row-head > .list-header-subject > .list-row-col > .list-check-all"
).check({ force: true });
cy.wait(3000);
cy.get(".actions-btn-group > .btn-primary").click({ force: true });
cy.wait(3000);
cy.get(".dropdown-menu-right > .user-action > .dropdown-item")
.contains("Sales Invoice")
.click({ force: true });
cy.wait(3000);
cy.get(".modal-content > .modal-footer > .standard-actions")
.contains("Yes")
.click({ force: true });
cy.contains("Creation of Sales Invoice successful");
});
});

View File

@@ -2,8 +2,6 @@ import inspect
import frappe import frappe
from erpnext.hooks import regional_overrides
__version__ = '14.0.0-dev' __version__ = '14.0.0-dev'
def get_default_company(user=None): def get_default_company(user=None):
@@ -121,14 +119,17 @@ def allow_regional(fn):
@erpnext.allow_regional @erpnext.allow_regional
def myfunction(): def myfunction():
pass''' pass'''
def caller(*args, **kwargs): def caller(*args, **kwargs):
region = get_region() overrides = frappe.get_hooks("regional_overrides", {}).get(get_region())
fn_name = inspect.getmodule(fn).__name__ + '.' + fn.__name__ function_path = f"{inspect.getmodule(fn).__name__}.{fn.__name__}"
if region in regional_overrides and fn_name in regional_overrides[region]:
return frappe.get_attr(regional_overrides[region][fn_name])(*args, **kwargs) if not overrides or function_path not in overrides:
else:
return fn(*args, **kwargs) return fn(*args, **kwargs)
# Priority given to last installed app
return frappe.get_attr(overrides[function_path][-1])(*args, **kwargs)
return caller return caller
def get_last_membership(member): def get_last_membership(member):

View File

@@ -7,35 +7,30 @@
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"accounts_transactions_settings_section", "invoice_and_billing_tab",
"over_billing_allowance", "enable_features_section",
"role_allowed_to_over_bill",
"credit_controller",
"make_payment_via_journal_entry",
"column_break_11",
"check_supplier_invoice_uniqueness",
"unlink_payment_on_cancellation_of_invoice", "unlink_payment_on_cancellation_of_invoice",
"automatically_fetch_payment_terms",
"delete_linked_ledger_entries",
"book_asset_depreciation_entry_automatically",
"unlink_advance_payment_on_cancelation_of_order", "unlink_advance_payment_on_cancelation_of_order",
"column_break_13",
"delete_linked_ledger_entries",
"invoicing_features_section",
"check_supplier_invoice_uniqueness",
"automatically_fetch_payment_terms",
"column_break_17",
"enable_common_party_accounting", "enable_common_party_accounting",
"post_change_gl_entries",
"enable_discount_accounting", "enable_discount_accounting",
"tax_settings_section", "report_setting_section",
"determine_address_tax_category_from", "use_custom_cash_flow",
"column_break_19",
"add_taxes_from_item_tax_template",
"period_closing_settings_section",
"acc_frozen_upto",
"frozen_accounts_modifier",
"column_break_4",
"deferred_accounting_settings_section", "deferred_accounting_settings_section",
"book_deferred_entries_based_on", "book_deferred_entries_based_on",
"column_break_18", "column_break_18",
"automatically_process_deferred_accounting_entry", "automatically_process_deferred_accounting_entry",
"book_deferred_entries_via_journal_entry", "book_deferred_entries_via_journal_entry",
"submit_journal_entries", "submit_journal_entries",
"tax_settings_section",
"determine_address_tax_category_from",
"column_break_19",
"add_taxes_from_item_tax_template",
"print_settings", "print_settings",
"show_inclusive_tax_in_print", "show_inclusive_tax_in_print",
"column_break_12", "column_break_12",
@@ -43,8 +38,25 @@
"currency_exchange_section", "currency_exchange_section",
"allow_stale", "allow_stale",
"stale_days", "stale_days",
"report_settings_sb", "invoicing_settings_tab",
"use_custom_cash_flow" "accounts_transactions_settings_section",
"over_billing_allowance",
"column_break_11",
"role_allowed_to_over_bill",
"credit_controller",
"make_payment_via_journal_entry",
"pos_tab",
"pos_setting_section",
"post_change_gl_entries",
"assets_tab",
"asset_settings_section",
"book_asset_depreciation_entry_automatically",
"closing_settings_tab",
"period_closing_settings_section",
"acc_frozen_upto",
"column_break_25",
"frozen_accounts_modifier",
"report_settings_sb"
], ],
"fields": [ "fields": [
{ {
@@ -70,10 +82,6 @@
"label": "Determine Address Tax Category From", "label": "Determine Address Tax Category From",
"options": "Billing Address\nShipping Address" "options": "Billing Address\nShipping Address"
}, },
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{ {
"fieldname": "credit_controller", "fieldname": "credit_controller",
"fieldtype": "Link", "fieldtype": "Link",
@@ -83,6 +91,7 @@
}, },
{ {
"default": "0", "default": "0",
"description": "Enabling ensure each Sales Invoice has a unique value in Supplier Invoice No. field",
"fieldname": "check_supplier_invoice_uniqueness", "fieldname": "check_supplier_invoice_uniqueness",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Check Supplier Invoice Number Uniqueness" "label": "Check Supplier Invoice Number Uniqueness"
@@ -168,7 +177,7 @@
"description": "Only select this if you have set up the Cash Flow Mapper documents", "description": "Only select this if you have set up the Cash Flow Mapper documents",
"fieldname": "use_custom_cash_flow", "fieldname": "use_custom_cash_flow",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Use Custom Cash Flow Format" "label": "Enable Custom Cash Flow Format"
}, },
{ {
"default": "0", "default": "0",
@@ -241,7 +250,7 @@
{ {
"fieldname": "accounts_transactions_settings_section", "fieldname": "accounts_transactions_settings_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Transactions Settings" "label": "Credit Limit Settings"
}, },
{ {
"fieldname": "column_break_11", "fieldname": "column_break_11",
@@ -272,9 +281,72 @@
}, },
{ {
"default": "0", "default": "0",
"description": "Learn about <a href=\"https://docs.erpnext.com/docs/v13/user/manual/en/accounts/articles/common_party_accounting#:~:text=Common%20Party%20Accounting%20in%20ERPNext,Invoice%20against%20a%20primary%20Supplier.\">Common Party</a>",
"fieldname": "enable_common_party_accounting", "fieldname": "enable_common_party_accounting",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Enable Common Party Accounting" "label": "Enable Common Party Accounting"
},
{
"fieldname": "enable_features_section",
"fieldtype": "Section Break",
"label": "Invoice Cancellation"
},
{
"fieldname": "column_break_13",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_25",
"fieldtype": "Column Break"
},
{
"fieldname": "asset_settings_section",
"fieldtype": "Section Break",
"label": "Asset Settings"
},
{
"fieldname": "invoicing_settings_tab",
"fieldtype": "Tab Break",
"label": "Credit Limits"
},
{
"fieldname": "assets_tab",
"fieldtype": "Tab Break",
"label": "Assets"
},
{
"fieldname": "closing_settings_tab",
"fieldtype": "Tab Break",
"label": "Accounts Closing"
},
{
"fieldname": "pos_setting_section",
"fieldtype": "Section Break",
"label": "POS Setting"
},
{
"fieldname": "invoice_and_billing_tab",
"fieldtype": "Tab Break",
"label": "Invoice and Billing"
},
{
"fieldname": "invoicing_features_section",
"fieldtype": "Section Break",
"label": "Invoicing Features"
},
{
"fieldname": "column_break_17",
"fieldtype": "Column Break"
},
{
"fieldname": "pos_tab",
"fieldtype": "Tab Break",
"label": "POS"
},
{
"fieldname": "report_setting_section",
"fieldtype": "Section Break",
"label": "Report Setting"
} }
], ],
"icon": "icon-cog", "icon": "icon-cog",
@@ -282,7 +354,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2021-10-11 17:42:36.427699", "modified": "2022-02-04 12:32:36.805652",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Settings", "name": "Accounts Settings",
@@ -309,5 +381,6 @@
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC", "sort_order": "ASC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -14,6 +14,10 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
}); });
}, },
onload: function (frm) {
frm.trigger('bank_account');
},
refresh: function (frm) { refresh: function (frm) {
frappe.require("bank-reconciliation-tool.bundle.js", () => frappe.require("bank-reconciliation-tool.bundle.js", () =>
frm.trigger("make_reconciliation_tool") frm.trigger("make_reconciliation_tool")
@@ -51,7 +55,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
bank_account: function (frm) { bank_account: function (frm) {
frappe.db.get_value( frappe.db.get_value(
"Bank Account", "Bank Account",
frm.bank_account, frm.doc.bank_account,
"account", "account",
(r) => { (r) => {
frappe.db.get_value( frappe.db.get_value(
@@ -60,6 +64,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
"account_currency", "account_currency",
(r) => { (r) => {
frm.currency = r.account_currency; frm.currency = r.account_currency;
frm.trigger("render_chart");
} }
); );
} }
@@ -124,7 +129,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
} }
}, },
render_chart(frm) { render_chart: frappe.utils.debounce((frm) => {
frm.cards_manager = new erpnext.accounts.bank_reconciliation.NumberCardManager( frm.cards_manager = new erpnext.accounts.bank_reconciliation.NumberCardManager(
{ {
$reconciliation_tool_cards: frm.get_field( $reconciliation_tool_cards: frm.get_field(
@@ -136,7 +141,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
currency: frm.currency, currency: frm.currency,
} }
); );
}, }, 500),
render(frm) { render(frm) {
if (frm.doc.bank_account) { if (frm.doc.bank_account) {

View File

@@ -7,6 +7,7 @@ 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.query_builder.custom import ConstantColumn
from frappe.utils import flt from frappe.utils import flt
from erpnext import get_company_currency from erpnext import get_company_currency
@@ -275,6 +276,10 @@ def check_matching(bank_account, company, transaction, document_types):
} }
matching_vouchers = [] matching_vouchers = []
matching_vouchers.extend(get_loan_vouchers(bank_account, transaction,
document_types, filters))
for query in subquery: for query in subquery:
matching_vouchers.extend( matching_vouchers.extend(
frappe.db.sql(query, filters,) frappe.db.sql(query, filters,)
@@ -311,6 +316,114 @@ def get_queries(bank_account, company, transaction, document_types):
return queries return queries
def get_loan_vouchers(bank_account, transaction, document_types, filters):
vouchers = []
amount_condition = True if "exact_match" in document_types else False
if transaction.withdrawal > 0 and "loan_disbursement" in document_types:
vouchers.extend(get_ld_matching_query(bank_account, amount_condition, filters))
if transaction.deposit > 0 and "loan_repayment" in document_types:
vouchers.extend(get_lr_matching_query(bank_account, amount_condition, filters))
return vouchers
def get_ld_matching_query(bank_account, amount_condition, filters):
loan_disbursement = frappe.qb.DocType("Loan Disbursement")
matching_reference = loan_disbursement.reference_number == filters.get("reference_number")
matching_party = loan_disbursement.applicant_type == filters.get("party_type") and \
loan_disbursement.applicant == filters.get("party")
rank = (
frappe.qb.terms.Case()
.when(matching_reference, 1)
.else_(0)
)
rank1 = (
frappe.qb.terms.Case()
.when(matching_party, 1)
.else_(0)
)
query = frappe.qb.from_(loan_disbursement).select(
rank + rank1 + 1,
ConstantColumn("Loan Disbursement").as_("doctype"),
loan_disbursement.name,
loan_disbursement.disbursed_amount,
loan_disbursement.reference_number,
loan_disbursement.reference_date,
loan_disbursement.applicant_type,
loan_disbursement.disbursement_date
).where(
loan_disbursement.docstatus == 1
).where(
loan_disbursement.clearance_date.isnull()
).where(
loan_disbursement.disbursement_account == bank_account
)
if amount_condition:
query.where(
loan_disbursement.disbursed_amount == filters.get('amount')
)
else:
query.where(
loan_disbursement.disbursed_amount <= filters.get('amount')
)
vouchers = query.run(as_list=True)
return vouchers
def get_lr_matching_query(bank_account, amount_condition, filters):
loan_repayment = frappe.qb.DocType("Loan Repayment")
matching_reference = loan_repayment.reference_number == filters.get("reference_number")
matching_party = loan_repayment.applicant_type == filters.get("party_type") and \
loan_repayment.applicant == filters.get("party")
rank = (
frappe.qb.terms.Case()
.when(matching_reference, 1)
.else_(0)
)
rank1 = (
frappe.qb.terms.Case()
.when(matching_party, 1)
.else_(0)
)
query = frappe.qb.from_(loan_repayment).select(
rank + rank1 + 1,
ConstantColumn("Loan Repayment").as_("doctype"),
loan_repayment.name,
loan_repayment.amount_paid,
loan_repayment.reference_number,
loan_repayment.reference_date,
loan_repayment.applicant_type,
loan_repayment.posting_date
).where(
loan_repayment.docstatus == 1
).where(
loan_repayment.clearance_date.isnull()
).where(
loan_repayment.payment_account == bank_account
)
if amount_condition:
query.where(
loan_repayment.amount_paid == filters.get('amount')
)
else:
query.where(
loan_repayment.amount_paid <= filters.get('amount')
)
vouchers = query.run()
return vouchers
def get_pe_matching_query(amount_condition, account_from_to, transaction): def get_pe_matching_query(amount_condition, account_from_to, transaction):
# get matching payment entries query # get matching payment entries query
if transaction.deposit > 0: if transaction.deposit > 0:
@@ -348,7 +461,6 @@ def get_je_matching_query(amount_condition, transaction):
# We have mapping at the bank level # We have mapping at the bank level
# So one bank could have both types of bank accounts like asset and liability # So one bank could have both types of bank accounts like asset and liability
# So cr_or_dr should be judged only on basis of withdrawal and deposit and not account type # So cr_or_dr should be judged only on basis of withdrawal and deposit and not account type
company_account = frappe.get_value("Bank Account", transaction.bank_account, "account")
cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit" cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
return f""" return f"""

View File

@@ -2,9 +2,10 @@
# For license information, please see license.txt # For license information, please see license.txt
from functools import reduce
import frappe import frappe
from frappe.utils import flt from frappe.utils import flt
from six.moves import reduce
from erpnext.controllers.status_updater import StatusUpdater from erpnext.controllers.status_updater import StatusUpdater
@@ -48,7 +49,8 @@ class BankTransaction(StatusUpdater):
def clear_linked_payment_entries(self, for_cancel=False): def clear_linked_payment_entries(self, for_cancel=False):
for payment_entry in self.payment_entries: for payment_entry in self.payment_entries:
if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim"]: if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim", "Loan Repayment",
"Loan Disbursement"]:
self.clear_simple_entry(payment_entry, for_cancel=for_cancel) self.clear_simple_entry(payment_entry, for_cancel=for_cancel)
elif payment_entry.payment_document == "Sales Invoice": elif payment_entry.payment_document == "Sales Invoice":
@@ -115,11 +117,18 @@ def get_paid_amount(payment_entry, currency, bank_account):
payment_entry.payment_entry, paid_amount_field) payment_entry.payment_entry, paid_amount_field)
elif payment_entry.payment_document == "Journal Entry": elif payment_entry.payment_document == "Journal Entry":
return frappe.db.get_value('Journal Entry Account', {'parent': payment_entry.payment_entry, 'account': bank_account}, "sum(credit_in_account_currency)") return frappe.db.get_value('Journal Entry Account', {'parent': payment_entry.payment_entry, 'account': bank_account},
"sum(credit_in_account_currency)")
elif payment_entry.payment_document == "Expense Claim": elif payment_entry.payment_document == "Expense Claim":
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_amount_reimbursed") return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_amount_reimbursed")
elif payment_entry.payment_document == "Loan Disbursement":
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "disbursed_amount")
elif payment_entry.payment_document == "Loan Repayment":
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "amount_paid")
else: else:
frappe.throw("Please reconcile {0}: {1} manually".format(payment_entry.payment_document, payment_entry.payment_entry)) frappe.throw("Please reconcile {0}: {1} manually".format(payment_entry.payment_document, payment_entry.payment_entry))

View File

@@ -109,7 +109,7 @@ def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"):
frappe.get_doc({ frappe.get_doc({
"doctype": "Bank", "doctype": "Bank",
"bank_name":bank_name, "bank_name":bank_name,
}).insert() }).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
@@ -119,7 +119,7 @@ def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"):
"account_name":"Checking Account", "account_name":"Checking Account",
"bank": bank_name, "bank": bank_name,
"account": account_name "account": account_name
}).insert() }).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
@@ -184,7 +184,7 @@ def add_vouchers():
"supplier_group":"All Supplier Groups", "supplier_group":"All Supplier Groups",
"supplier_type": "Company", "supplier_type": "Company",
"supplier_name": "Conrad Electronic" "supplier_name": "Conrad Electronic"
}).insert() }).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
@@ -203,7 +203,7 @@ def add_vouchers():
"supplier_group":"All Supplier Groups", "supplier_group":"All Supplier Groups",
"supplier_type": "Company", "supplier_type": "Company",
"supplier_name": "Mr G" "supplier_name": "Mr G"
}).insert() }).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
@@ -227,7 +227,7 @@ def add_vouchers():
"supplier_group":"All Supplier Groups", "supplier_group":"All Supplier Groups",
"supplier_type": "Company", "supplier_type": "Company",
"supplier_name": "Poore Simon's" "supplier_name": "Poore Simon's"
}).insert() }).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
@@ -237,7 +237,7 @@ def add_vouchers():
"customer_group":"All Customer Groups", "customer_group":"All Customer Groups",
"customer_type": "Company", "customer_type": "Company",
"customer_name": "Poore Simon's" "customer_name": "Poore Simon's"
}).insert() }).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
@@ -266,7 +266,7 @@ def add_vouchers():
"customer_group":"All Customer Groups", "customer_group":"All Customer Groups",
"customer_type": "Company", "customer_type": "Company",
"customer_name": "Fayva" "customer_name": "Fayva"
}).insert() }).insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass

View File

@@ -1,94 +1,34 @@
{ {
"allow_copy": 0, "actions": [],
"allow_guest_to_view": 0, "creation": "2018-02-08 10:18:48.513608",
"allow_import": 0, "doctype": "DocType",
"allow_rename": 0, "editable_grid": 1,
"autoname": "field:mapping", "engine": "InnoDB",
"beta": 0, "field_order": [
"creation": "2018-02-08 10:18:48.513608", "mapping"
"custom": 0, ],
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "fieldname": "mapping",
"allow_on_submit": 0, "fieldtype": "Link",
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Mapping",
"columns": 0, "options": "Cash Flow Mapping",
"fieldname": "mapping", "reqd": 1,
"fieldtype": "Link", "unique": 1
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Mapping",
"length": 0,
"no_copy": 0,
"options": "Cash Flow Mapping",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
} }
], ],
"has_web_view": 0, "istable": 1,
"hide_heading": 0, "links": [],
"hide_toolbar": 0, "modified": "2022-02-21 03:34:57.902332",
"idx": 0, "modified_by": "Administrator",
"image_view": 0, "module": "Accounts",
"in_create": 0, "name": "Cash Flow Mapping Template Details",
"is_submittable": 0, "owner": "Administrator",
"issingle": 0, "permissions": [],
"istable": 0, "quick_entry": 1,
"max_attachments": 0, "sort_field": "modified",
"modified": "2018-02-08 10:33:39.413930", "sort_order": "DESC",
"modified_by": "Administrator", "states": [],
"module": "Accounts", "track_changes": 1
"name": "Cash Flow Mapping Template Details",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
} }

View File

@@ -15,17 +15,6 @@ frappe.ui.form.on('Cost Center', {
} }
} }
}); });
frm.set_query("cost_center", "distributed_cost_center", function() {
return {
filters: {
company: frm.doc.company,
is_group: 0,
enable_distributed_cost_center: 0,
name: ['!=', frm.doc.name]
}
};
});
}, },
refresh: function(frm) { refresh: function(frm) {
if (!frm.is_new()) { if (!frm.is_new()) {

View File

@@ -16,9 +16,6 @@
"cb0", "cb0",
"is_group", "is_group",
"disabled", "disabled",
"section_break_9",
"enable_distributed_cost_center",
"distributed_cost_center",
"lft", "lft",
"rgt", "rgt",
"old_parent" "old_parent"
@@ -122,31 +119,13 @@
"fieldname": "disabled", "fieldname": "disabled",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Disabled" "label": "Disabled"
},
{
"default": "0",
"fieldname": "enable_distributed_cost_center",
"fieldtype": "Check",
"label": "Enable Distributed Cost Center"
},
{
"depends_on": "eval:doc.is_group==0",
"fieldname": "section_break_9",
"fieldtype": "Section Break"
},
{
"depends_on": "enable_distributed_cost_center",
"fieldname": "distributed_cost_center",
"fieldtype": "Table",
"label": "Distributed Cost Center",
"options": "Distributed Cost Center"
} }
], ],
"icon": "fa fa-money", "icon": "fa fa-money",
"idx": 1, "idx": 1,
"is_tree": 1, "is_tree": 1,
"links": [], "links": [],
"modified": "2020-06-17 16:09:30.025214", "modified": "2022-01-31 13:22:58.916273",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Cost Center", "name": "Cost Center",
@@ -189,5 +168,6 @@
"search_fields": "parent_cost_center, is_group", "search_fields": "parent_cost_center, is_group",
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC" "sort_order": "ASC",
"states": []
} }

View File

@@ -4,7 +4,6 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import cint
from frappe.utils.nestedset import NestedSet from frappe.utils.nestedset import NestedSet
from erpnext.accounts.utils import validate_field_number from erpnext.accounts.utils import validate_field_number
@@ -20,24 +19,6 @@ class CostCenter(NestedSet):
def validate(self): def validate(self):
self.validate_mandatory() self.validate_mandatory()
self.validate_parent_cost_center() self.validate_parent_cost_center()
self.validate_distributed_cost_center()
def validate_distributed_cost_center(self):
if cint(self.enable_distributed_cost_center):
if not self.distributed_cost_center:
frappe.throw(_("Please enter distributed cost center"))
if sum(x.percentage_allocation for x in self.distributed_cost_center) != 100:
frappe.throw(_("Total percentage allocation for distributed cost center should be equal to 100"))
if not self.get('__islocal'):
if not cint(frappe.get_cached_value("Cost Center", {"name": self.name}, "enable_distributed_cost_center")) \
and self.check_if_part_of_distributed_cost_center():
frappe.throw(_("Cannot enable Distributed Cost Center for a Cost Center already allocated in another Distributed Cost Center"))
if next((True for x in self.distributed_cost_center if x.cost_center == x.parent), False):
frappe.throw(_("Parent Cost Center cannot be added in Distributed Cost Center"))
if check_if_distributed_cost_center_enabled(list(x.cost_center for x in self.distributed_cost_center)):
frappe.throw(_("A Distributed Cost Center cannot be added in the Distributed Cost Center allocation table."))
else:
self.distributed_cost_center = []
def validate_mandatory(self): def validate_mandatory(self):
if self.cost_center_name != self.company and not self.parent_cost_center: if self.cost_center_name != self.company and not self.parent_cost_center:
@@ -64,10 +45,10 @@ class CostCenter(NestedSet):
@frappe.whitelist() @frappe.whitelist()
def convert_ledger_to_group(self): def convert_ledger_to_group(self):
if cint(self.enable_distributed_cost_center): if self.if_allocation_exists_against_cost_center():
frappe.throw(_("Cost Center with enabled distributed cost center can not be converted to group")) frappe.throw(_("Cost Center with Allocation records can not be converted to a group"))
if self.check_if_part_of_distributed_cost_center(): if self.check_if_part_of_cost_center_allocation():
frappe.throw(_("Cost Center Already Allocated in a Distributed Cost Center cannot be converted to group")) frappe.throw(_("Cost Center is a part of Cost Center Allocation, hence cannot be converted to a group"))
if self.check_gle_exists(): if self.check_gle_exists():
frappe.throw(_("Cost Center with existing transactions can not be converted to group")) frappe.throw(_("Cost Center with existing transactions can not be converted to group"))
self.is_group = 1 self.is_group = 1
@@ -81,8 +62,17 @@ class CostCenter(NestedSet):
return frappe.db.sql("select name from `tabCost Center` where \ return frappe.db.sql("select name from `tabCost Center` where \
parent_cost_center = %s and docstatus != 2", self.name) parent_cost_center = %s and docstatus != 2", self.name)
def check_if_part_of_distributed_cost_center(self): def if_allocation_exists_against_cost_center(self):
return frappe.db.get_value("Distributed Cost Center", {"cost_center": self.name}) return frappe.db.get_value("Cost Center Allocation", filters = {
"main_cost_center": self.name,
"docstatus": 1
})
def check_if_part_of_cost_center_allocation(self):
return frappe.db.get_value("Cost Center Allocation Percentage", filters = {
"cost_center": self.name,
"docstatus": 1
})
def before_rename(self, olddn, newdn, merge=False): def before_rename(self, olddn, newdn, merge=False):
# Add company abbr if not provided # Add company abbr if not provided
@@ -126,8 +116,4 @@ def on_doctype_update():
def get_name_with_number(new_account, account_number): def get_name_with_number(new_account, account_number):
if account_number and not new_account[0].isdigit(): if account_number and not new_account[0].isdigit():
new_account = account_number + " - " + new_account new_account = account_number + " - " + new_account
return new_account return new_account
def check_if_distributed_cost_center_enabled(cost_center_list):
value_list = frappe.get_list("Cost Center", {"name": ["in", cost_center_list]}, "enable_distributed_cost_center", as_list=1)
return next((True for x in value_list if x[0]), False)

View File

@@ -23,33 +23,6 @@ class TestCostCenter(unittest.TestCase):
self.assertRaises(frappe.ValidationError, cost_center.save) self.assertRaises(frappe.ValidationError, cost_center.save)
def test_validate_distributed_cost_center(self):
if not frappe.db.get_value('Cost Center', {'name': '_Test Cost Center - _TC'}):
frappe.get_doc(test_records[0]).insert()
if not frappe.db.get_value('Cost Center', {'name': '_Test Cost Center 2 - _TC'}):
frappe.get_doc(test_records[1]).insert()
invalid_distributed_cost_center = frappe.get_doc({
"company": "_Test Company",
"cost_center_name": "_Test Distributed Cost Center",
"doctype": "Cost Center",
"is_group": 0,
"parent_cost_center": "_Test Company - _TC",
"enable_distributed_cost_center": 1,
"distributed_cost_center": [{
"cost_center": "_Test Cost Center - _TC",
"percentage_allocation": 40
}, {
"cost_center": "_Test Cost Center 2 - _TC",
"percentage_allocation": 50
}
]
})
self.assertRaises(frappe.ValidationError, invalid_distributed_cost_center.save)
def create_cost_center(**args): def create_cost_center(**args):
args = frappe._dict(args) args = frappe._dict(args)
if args.cost_center_name: if args.cost_center_name:

View File

@@ -0,0 +1,19 @@
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Cost Center Allocation', {
setup: function(frm) {
let filters = {"is_group": 0};
if (frm.doc.company) {
$.extend(filters, {
"company": frm.doc.company
});
}
frm.set_query('main_cost_center', function() {
return {
filters: filters
};
});
}
});

View File

@@ -0,0 +1,128 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "CC-ALLOC-.#####",
"creation": "2022-01-13 20:07:29.871109",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"main_cost_center",
"company",
"column_break_2",
"valid_from",
"section_break_5",
"allocation_percentages",
"amended_from"
],
"fields": [
{
"fieldname": "main_cost_center",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Main Cost Center",
"options": "Cost Center",
"reqd": 1
},
{
"default": "Today",
"fieldname": "valid_from",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Valid From",
"reqd": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_5",
"fieldtype": "Section Break"
},
{
"fetch_from": "main_cost_center.company",
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "allocation_percentages",
"fieldtype": "Table",
"label": "Cost Center Allocation Percentages",
"options": "Cost Center Allocation Percentage",
"reqd": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Cost Center Allocation",
"print_hide": 1,
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-01-31 11:47:12.086253",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Cost Center Allocation",
"name_case": "UPPER CASE",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1,
"submit": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@@ -0,0 +1,90 @@
# Copyright (c) 2022, 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 add_days, format_date, getdate
class MainCostCenterCantBeChild(frappe.ValidationError):
pass
class InvalidMainCostCenter(frappe.ValidationError):
pass
class InvalidChildCostCenter(frappe.ValidationError):
pass
class WrongPercentageAllocation(frappe.ValidationError):
pass
class InvalidDateError(frappe.ValidationError):
pass
class CostCenterAllocation(Document):
def validate(self):
self.validate_total_allocation_percentage()
self.validate_from_date_based_on_existing_gle()
self.validate_backdated_allocation()
self.validate_main_cost_center()
self.validate_child_cost_centers()
def validate_total_allocation_percentage(self):
total_percentage = sum([d.percentage for d in self.get("allocation_percentages", [])])
if total_percentage != 100:
frappe.throw(_("Total percentage against cost centers should be 100"), WrongPercentageAllocation)
def validate_from_date_based_on_existing_gle(self):
# Check if GLE exists against the main cost center
# If exists ensure from date is set after posting date of last GLE
last_gle_date = frappe.db.get_value("GL Entry",
{"cost_center": self.main_cost_center, "is_cancelled": 0},
"posting_date", order_by="posting_date desc")
if last_gle_date:
if getdate(self.valid_from) <= getdate(last_gle_date):
frappe.throw(_("Valid From must be after {0} as last GL Entry against the cost center {1} posted on this date")
.format(last_gle_date, self.main_cost_center), InvalidDateError)
def validate_backdated_allocation(self):
# Check if there are any future existing allocation records against the main cost center
# If exists, warn the user about it
future_allocation = frappe.db.get_value("Cost Center Allocation", filters = {
"main_cost_center": self.main_cost_center,
"valid_from": (">=", self.valid_from),
"name": ("!=", self.name),
"docstatus": 1
}, fieldname=['valid_from', 'name'], order_by='valid_from', as_dict=1)
if future_allocation:
frappe.msgprint(_("Another Cost Center Allocation record {0} applicable from {1}, hence this allocation will be applicable upto {2}")
.format(frappe.bold(future_allocation.name), frappe.bold(format_date(future_allocation.valid_from)),
frappe.bold(format_date(add_days(future_allocation.valid_from, -1)))),
title=_("Warning!"), indicator="orange", alert=1
)
def validate_main_cost_center(self):
# Main cost center itself cannot be entered in child table
if self.main_cost_center in [d.cost_center for d in self.allocation_percentages]:
frappe.throw(_("Main Cost Center {0} cannot be entered in the child table")
.format(self.main_cost_center), MainCostCenterCantBeChild)
# If main cost center is used for allocation under any other cost center,
# allocation cannot be done against it
parent = frappe.db.get_value("Cost Center Allocation Percentage", filters = {
"cost_center": self.main_cost_center,
"docstatus": 1
}, fieldname='parent')
if parent:
frappe.throw(_("{0} cannot be used as a Main Cost Center because it has been used as child in Cost Center Allocation {1}")
.format(self.main_cost_center, parent), InvalidMainCostCenter)
def validate_child_cost_centers(self):
# Check if child cost center is used as main cost center in any existing allocation
main_cost_centers = [d.main_cost_center for d in
frappe.get_all("Cost Center Allocation", {'docstatus': 1}, 'main_cost_center')]
for d in self.allocation_percentages:
if d.cost_center in main_cost_centers:
frappe.throw(_("Cost Center {0} cannot be used for allocation as it is used as main cost center in other allocation record.")
.format(d.cost_center), InvalidChildCostCenter)

View File

@@ -0,0 +1,156 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
from frappe.utils import add_days, today
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
from erpnext.accounts.doctype.cost_center_allocation.cost_center_allocation import (
InvalidChildCostCenter,
InvalidDateError,
InvalidMainCostCenter,
MainCostCenterCantBeChild,
WrongPercentageAllocation,
)
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
class TestCostCenterAllocation(unittest.TestCase):
def setUp(self):
cost_centers = ["Main Cost Center 1", "Main Cost Center 2", "Sub Cost Center 1", "Sub Cost Center 2"]
for cc in cost_centers:
create_cost_center(cost_center_name=cc, company="_Test Company")
def test_gle_based_on_cost_center_allocation(self):
cca = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
{
"Sub Cost Center 1 - _TC": 60,
"Sub Cost Center 2 - _TC": 40
}
)
jv = make_journal_entry("_Test Cash - _TC", "Sales - _TC", 100,
cost_center = "Main Cost Center 1 - _TC", submit=True)
expected_values = [
["Sub Cost Center 1 - _TC", 0.0, 60],
["Sub Cost Center 2 - _TC", 0.0, 40]
]
gle = frappe.qb.DocType("GL Entry")
gl_entries = (
frappe.qb.from_(gle)
.select(gle.cost_center, gle.debit, gle.credit)
.where(gle.voucher_type == 'Journal Entry')
.where(gle.voucher_no == jv.name)
.where(gle.account == 'Sales - _TC')
.orderby(gle.cost_center)
).run(as_dict=1)
self.assertTrue(gl_entries)
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[i][0], gle.cost_center)
self.assertEqual(expected_values[i][1], gle.debit)
self.assertEqual(expected_values[i][2], gle.credit)
cca.cancel()
jv.cancel()
def test_main_cost_center_cant_be_child(self):
# Main cost center itself cannot be entered in child table
cca = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
{
"Sub Cost Center 1 - _TC": 60,
"Main Cost Center 1 - _TC": 40
}, save=False
)
self.assertRaises(MainCostCenterCantBeChild, cca.save)
def test_invalid_main_cost_center(self):
# If main cost center is used for allocation under any other cost center,
# allocation cannot be done against it
cca1 = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
{
"Sub Cost Center 1 - _TC": 60,
"Sub Cost Center 2 - _TC": 40
}
)
cca2 = create_cost_center_allocation("_Test Company", "Sub Cost Center 1 - _TC",
{
"Sub Cost Center 2 - _TC": 100
}, save=False
)
self.assertRaises(InvalidMainCostCenter, cca2.save)
cca1.cancel()
def test_if_child_cost_center_has_any_allocation_record(self):
# Check if any child cost center is used as main cost center in any other existing allocation
cca1 = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
{
"Sub Cost Center 1 - _TC": 60,
"Sub Cost Center 2 - _TC": 40
}
)
cca2 = create_cost_center_allocation("_Test Company", "Main Cost Center 2 - _TC",
{
"Main Cost Center 1 - _TC": 60,
"Sub Cost Center 1 - _TC": 40
}, save=False
)
self.assertRaises(InvalidChildCostCenter, cca2.save)
cca1.cancel()
def test_total_percentage(self):
cca = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
{
"Sub Cost Center 1 - _TC": 40,
"Sub Cost Center 2 - _TC": 40
}, save=False
)
self.assertRaises(WrongPercentageAllocation, cca.save)
def test_valid_from_based_on_existing_gle(self):
# GLE posted against Sub Cost Center 1 on today
jv = make_journal_entry("_Test Cash - _TC", "Sales - _TC", 100,
cost_center = "Main Cost Center 1 - _TC", posting_date=today(), submit=True)
# try to set valid from as yesterday
cca = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
{
"Sub Cost Center 1 - _TC": 60,
"Sub Cost Center 2 - _TC": 40
}, valid_from=add_days(today(), -1), save=False
)
self.assertRaises(InvalidDateError, cca.save)
jv.cancel()
def create_cost_center_allocation(company, main_cost_center, allocation_percentages,
valid_from=None, valid_upto=None, save=True, submit=True):
doc = frappe.new_doc("Cost Center Allocation")
doc.main_cost_center = main_cost_center
doc.company = company
doc.valid_from = valid_from or today()
doc.valid_upto = valid_upto
for cc, percentage in allocation_percentages.items():
doc.append("allocation_percentages", {
"cost_center": cc,
"percentage": percentage
})
if save:
doc.save()
if submit:
doc.submit()
return doc

View File

@@ -1,12 +1,13 @@
{ {
"actions": [], "actions": [],
"creation": "2020-03-19 12:34:01.500390", "allow_rename": 1,
"creation": "2022-01-13 20:07:30.096306",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"cost_center", "cost_center",
"percentage_allocation" "percentage"
], ],
"fields": [ "fields": [
{ {
@@ -18,23 +19,23 @@
"reqd": 1 "reqd": 1
}, },
{ {
"fieldname": "percentage_allocation", "fieldname": "percentage",
"fieldtype": "Float", "fieldtype": "Percent",
"in_list_view": 1, "in_list_view": 1,
"label": "Percentage Allocation", "label": "Percentage (%)",
"reqd": 1 "reqd": 1
} }
], ],
"index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-03-19 12:54:43.674655", "modified": "2022-02-01 22:22:31.589523",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Distributed Cost Center", "name": "Cost Center Allocation Percentage",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1 "states": []
} }

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class CostCenterAllocationPercentage(Document):
pass

View File

@@ -39,9 +39,6 @@ def test_create_test_data():
"selling_cost_center": "Main - _TC", "selling_cost_center": "Main - _TC",
"income_account": "Sales - _TC" "income_account": "Sales - _TC"
}], }],
"show_in_website": 1,
"route":"-test-tesla-car",
"website_warehouse": "Stores - _TC"
}) })
item.insert() item.insert()
# create test item price # create test item price

View File

@@ -2,7 +2,7 @@
"actions": [], "actions": [],
"allow_import": 1, "allow_import": 1,
"allow_rename": 1, "allow_rename": 1,
"creation": "2018-11-22 22:45:00.370913", "creation": "2022-01-19 01:09:13.297137",
"doctype": "DocType", "doctype": "DocType",
"document_type": "Setup", "document_type": "Setup",
"editable_grid": 1, "editable_grid": 1,
@@ -10,6 +10,9 @@
"field_order": [ "field_order": [
"title", "title",
"company", "company",
"column_break_3",
"disabled",
"section_break_5",
"taxes" "taxes"
], ],
"fields": [ "fields": [
@@ -36,10 +39,24 @@
"label": "Company", "label": "Company",
"options": "Company", "options": "Company",
"reqd": 1 "reqd": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled"
},
{
"fieldname": "section_break_5",
"fieldtype": "Section Break"
} }
], ],
"links": [], "links": [],
"modified": "2021-03-08 19:50:21.416513", "modified": "2022-01-18 21:11:23.105589",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Item Tax Template", "name": "Item Tax Template",
@@ -82,6 +99,7 @@
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"title_field": "title", "title_field": "title",
"track_changes": 1 "track_changes": 1
} }

View File

@@ -8,6 +8,7 @@ frappe.provide("erpnext.journal_entry");
frappe.ui.form.on("Journal Entry", { frappe.ui.form.on("Journal Entry", {
setup: function(frm) { setup: function(frm) {
frm.add_fetch("bank_account", "account", "account"); frm.add_fetch("bank_account", "account", "account");
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice'];
}, },
refresh: function(frm) { refresh: function(frm) {

View File

@@ -166,8 +166,9 @@ class OpeningInvoiceCreationTool(Document):
frappe.scrub(row.party_type): row.party, frappe.scrub(row.party_type): row.party,
"is_pos": 0, "is_pos": 0,
"doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice", "doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice",
"update_stock": 0, "update_stock": 0, # important: https://github.com/frappe/erpnext/pull/23559
"invoice_number": row.invoice_number "invoice_number": row.invoice_number,
"disable_rounded_total": 1
}) })
accounting_dimension = get_accounting_dimensions() accounting_dimension = get_accounting_dimensions()

View File

@@ -1,11 +1,7 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import unittest
import frappe import frappe
from frappe.cache_manager import clear_doctype_cache
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
create_dimension, create_dimension,
@@ -14,14 +10,17 @@ from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension imp
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import ( from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import (
get_temporary_opening_account, get_temporary_opening_account,
) )
from erpnext.tests.utils import ERPNextTestCase
test_dependencies = ["Customer", "Supplier", "Accounting Dimension"] test_dependencies = ["Customer", "Supplier", "Accounting Dimension"]
class TestOpeningInvoiceCreationTool(unittest.TestCase): class TestOpeningInvoiceCreationTool(ERPNextTestCase):
def setUp(self): @classmethod
def setUpClass(self):
if not frappe.db.exists("Company", "_Test Opening Invoice Company"): if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
make_company() make_company()
create_dimension() create_dimension()
return super().setUpClass()
def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None, invoice_number=None, department=None): def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None, invoice_number=None, department=None):
doc = frappe.get_single("Opening Invoice Creation Tool") doc = frappe.get_single("Opening Invoice Creation Tool")
@@ -31,26 +30,20 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
return doc.make_invoices() return doc.make_invoices()
def test_opening_sales_invoice_creation(self): def test_opening_sales_invoice_creation(self):
property_setter = make_property_setter("Sales Invoice", "update_stock", "default", 1, "Check") invoices = self.make_invoices(company="_Test Opening Invoice Company")
try:
invoices = self.make_invoices(company="_Test Opening Invoice Company")
self.assertEqual(len(invoices), 2) self.assertEqual(len(invoices), 2)
expected_value = { expected_value = {
"keys": ["customer", "outstanding_amount", "status"], "keys": ["customer", "outstanding_amount", "status"],
0: ["_Test Customer", 300, "Overdue"], 0: ["_Test Customer", 300, "Overdue"],
1: ["_Test Customer 1", 250, "Overdue"], 1: ["_Test Customer 1", 250, "Overdue"],
} }
self.check_expected_values(invoices, expected_value) self.check_expected_values(invoices, expected_value)
si = frappe.get_doc("Sales Invoice", invoices[0]) si = frappe.get_doc("Sales Invoice", invoices[0])
# Check if update stock is not enabled # Check if update stock is not enabled
self.assertEqual(si.update_stock, 0) self.assertEqual(si.update_stock, 0)
finally:
property_setter.delete()
clear_doctype_cache("Sales Invoice")
def check_expected_values(self, invoices, expected_value, invoice_type="Sales"): def check_expected_values(self, invoices, expected_value, invoice_type="Sales"):
doctype = "Sales Invoice" if invoice_type == "Sales" else "Purchase Invoice" doctype = "Sales Invoice" if invoice_type == "Sales" else "Purchase Invoice"

View File

@@ -1077,7 +1077,7 @@ def get_outstanding_reference_documents(args):
if d.voucher_type in ("Purchase Invoice"): if d.voucher_type in ("Purchase Invoice"):
d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no") d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no")
# Get all SO / PO which are not fully billed or aginst which full advance not paid # Get all SO / PO which are not fully billed or against which full advance not paid
orders_to_be_billed = [] orders_to_be_billed = []
if (args.get("party_type") != "Student"): if (args.get("party_type") != "Student"):
orders_to_be_billed = get_orders_to_be_billed(args.get("posting_date"),args.get("party_type"), orders_to_be_billed = get_orders_to_be_billed(args.get("posting_date"),args.get("party_type"),

View File

@@ -291,7 +291,7 @@ class PaymentRequest(Document):
if not status: if not status:
return return
shopping_cart_settings = frappe.get_doc("Shopping Cart Settings") shopping_cart_settings = frappe.get_doc("E Commerce Settings")
if status in ["Authorized", "Completed"]: if status in ["Authorized", "Completed"]:
redirect_to = None redirect_to = None
@@ -435,13 +435,13 @@ def get_existing_payment_request_amount(ref_dt, ref_dn):
""", (ref_dt, ref_dn)) """, (ref_dt, ref_dn))
return flt(existing_payment_request_amount[0][0]) if existing_payment_request_amount else 0 return flt(existing_payment_request_amount[0][0]) if existing_payment_request_amount else 0
def get_gateway_details(args): def get_gateway_details(args): # nosemgrep
"""return gateway and payment account of default payment gateway""" """return gateway and payment account of default payment gateway"""
if args.get("payment_gateway_account"): if args.get("payment_gateway_account"):
return get_payment_gateway_account(args.get("payment_gateway_account")) return get_payment_gateway_account(args.get("payment_gateway_account"))
if args.order_type == "Shopping Cart": if args.order_type == "Shopping Cart":
payment_gateway_account = frappe.get_doc("Shopping Cart Settings").payment_gateway_account payment_gateway_account = frappe.get_doc("E Commerce Settings").payment_gateway_account
return get_payment_gateway_account(payment_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})

View File

@@ -42,7 +42,6 @@ class POSInvoice(SalesInvoice):
self.validate_serialised_or_batched_item() self.validate_serialised_or_batched_item()
self.validate_stock_availablility() self.validate_stock_availablility()
self.validate_return_items_qty() self.validate_return_items_qty()
self.validate_non_stock_items()
self.set_status() self.set_status()
self.set_account_for_mode_of_payment() self.set_account_for_mode_of_payment()
self.validate_pos() self.validate_pos()
@@ -158,22 +157,40 @@ class POSInvoice(SalesInvoice):
frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another Sales Invoice. Please select valid serial no.") frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another Sales Invoice. Please select valid serial no.")
.format(item.idx, bold_delivered_serial_nos), title=_("Item Unavailable")) .format(item.idx, bold_delivered_serial_nos), title=_("Item Unavailable"))
def validate_invalid_serial_nos(self, item):
serial_nos = get_serial_nos(item.serial_no)
error_msg = []
invalid_serials, msg = "", ""
for serial_no in serial_nos:
if not frappe.db.exists('Serial No', serial_no):
invalid_serials = invalid_serials + (", " if invalid_serials else "") + serial_no
msg = (_("Row #{}: Following Serial numbers for item {} are <b>Invalid</b>: {}").format(item.idx, frappe.bold(item.get("item_code")), frappe.bold(invalid_serials)))
if invalid_serials:
error_msg.append(msg)
if error_msg:
frappe.throw(error_msg, title=_("Invalid Item"), as_list=True)
def validate_stock_availablility(self): def validate_stock_availablility(self):
from erpnext.stock.stock_ledger import is_negative_stock_allowed
if self.is_return or self.docstatus != 1: if self.is_return or self.docstatus != 1:
return return
allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock')
for d in self.get('items'): for d in self.get('items'):
is_service_item = not (frappe.db.get_value('Item', d.get('item_code'), 'is_stock_item'))
if is_service_item:
return
if d.serial_no: if d.serial_no:
self.validate_pos_reserved_serial_nos(d) self.validate_pos_reserved_serial_nos(d)
self.validate_delivered_serial_nos(d) self.validate_delivered_serial_nos(d)
self.validate_invalid_serial_nos(d)
elif d.batch_no: elif d.batch_no:
self.validate_pos_reserved_batch_qty(d) self.validate_pos_reserved_batch_qty(d)
else: else:
if allow_negative_stock: if is_negative_stock_allowed(item_code=d.item_code):
return return
available_stock = get_stock_availability(d.item_code, d.warehouse) available_stock, is_stock_item = get_stock_availability(d.item_code, d.warehouse)
item_code, warehouse, qty = frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty) item_code, warehouse, qty = frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty)
if flt(available_stock) <= 0: if flt(available_stock) <= 0:
@@ -244,14 +261,6 @@ class POSInvoice(SalesInvoice):
.format(d.idx, bold_serial_no, bold_return_against) .format(d.idx, bold_serial_no, bold_return_against)
) )
def validate_non_stock_items(self):
for d in self.get("items"):
is_stock_item = frappe.get_cached_value("Item", d.get("item_code"), "is_stock_item")
if not is_stock_item:
if not frappe.db.exists('Product Bundle', d.item_code):
frappe.throw(_("Row #{}: Item {} is a non stock item. You can only include stock items in a POS Invoice.")
.format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
def validate_mode_of_payment(self): def validate_mode_of_payment(self):
if len(self.payments) == 0: if len(self.payments) == 0:
frappe.throw(_("At least one mode of payment is required for POS invoice.")) frappe.throw(_("At least one mode of payment is required for POS invoice."))
@@ -430,7 +439,6 @@ class POSInvoice(SalesInvoice):
self.paid_amount = 0 self.paid_amount = 0
def set_account_for_mode_of_payment(self): def set_account_for_mode_of_payment(self):
self.payments = [d for d in self.payments if d.amount or d.base_amount or d.default]
for pay in self.payments: for pay in self.payments:
if not pay.account: if not pay.account:
pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account") pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account")
@@ -491,12 +499,18 @@ class POSInvoice(SalesInvoice):
@frappe.whitelist() @frappe.whitelist()
def get_stock_availability(item_code, warehouse): def get_stock_availability(item_code, warehouse):
if frappe.db.get_value('Item', item_code, 'is_stock_item'): if frappe.db.get_value('Item', item_code, 'is_stock_item'):
is_stock_item = True
bin_qty = get_bin_qty(item_code, warehouse) bin_qty = get_bin_qty(item_code, warehouse)
pos_sales_qty = get_pos_reserved_qty(item_code, warehouse) pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
return bin_qty - pos_sales_qty return bin_qty - pos_sales_qty, is_stock_item
else: else:
is_stock_item = False
if frappe.db.exists('Product Bundle', item_code): if frappe.db.exists('Product Bundle', item_code):
return get_bundle_availability(item_code, warehouse) return get_bundle_availability(item_code, warehouse), is_stock_item
else:
# Is a service item
return 0, is_stock_item
def get_bundle_availability(bundle_item_code, warehouse): def get_bundle_availability(bundle_item_code, warehouse):
product_bundle = frappe.get_doc('Product Bundle', bundle_item_code) product_bundle = frappe.get_doc('Product Bundle', bundle_item_code)

View File

@@ -354,6 +354,24 @@ class TestPOSInvoice(unittest.TestCase):
pos2.insert() pos2.insert()
self.assertRaises(frappe.ValidationError, pos2.submit) self.assertRaises(frappe.ValidationError, pos2.submit)
def test_invalid_serial_no_validation(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
se = make_serialized_item(company='_Test Company',
target_warehouse="Stores - _TC", cost_center='Main - _TC', expense_account='Cost of Goods Sold - _TC')
serial_nos = se.get("items")[0].serial_no + 'wrong'
pos = create_pos_invoice(company='_Test Company', debit_to='Debtors - _TC',
account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC',
expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC',
item=se.get("items")[0].item_code, rate=1000, qty=2, do_not_save=1)
pos.get('items')[0].has_serial_no = 1
pos.get('items')[0].serial_no = serial_nos
pos.insert()
self.assertRaises(frappe.ValidationError, pos.submit)
def test_loyalty_points(self): def test_loyalty_points(self):
from erpnext.accounts.doctype.loyalty_program.loyalty_program import ( from erpnext.accounts.doctype.loyalty_program.loyalty_program import (
get_loyalty_program_details_with_points, get_loyalty_program_details_with_points,
@@ -568,23 +586,29 @@ class TestPOSInvoice(unittest.TestCase):
item_price.insert() item_price.insert()
pr = make_pricing_rule(selling=1, priority=5, discount_percentage=10) pr = make_pricing_rule(selling=1, priority=5, discount_percentage=10)
pr.save() pr.save()
pos_inv = create_pos_invoice(qty=1, do_not_submit=1)
pos_inv.items[0].rate = 300
pos_inv.save()
self.assertEquals(pos_inv.items[0].discount_percentage, 10)
# rate shouldn't change
self.assertEquals(pos_inv.items[0].rate, 405)
pos_inv.ignore_pricing_rule = 1 try:
pos_inv.items[0].rate = 300 pos_inv = create_pos_invoice(qty=1, do_not_submit=1)
pos_inv.save() pos_inv.items[0].rate = 300
self.assertEquals(pos_inv.ignore_pricing_rule, 1) pos_inv.save()
# rate should change since pricing rules are ignored self.assertEquals(pos_inv.items[0].discount_percentage, 10)
self.assertEquals(pos_inv.items[0].rate, 300) # rate shouldn't change
self.assertEquals(pos_inv.items[0].rate, 405)
item_price.delete() pos_inv.ignore_pricing_rule = 1
pos_inv.delete() pos_inv.save()
pr.delete() self.assertEquals(pos_inv.ignore_pricing_rule, 1)
# rate should reset since pricing rules are ignored
self.assertEquals(pos_inv.items[0].rate, 450)
pos_inv.items[0].rate = 300
pos_inv.save()
self.assertEquals(pos_inv.items[0].rate, 300)
finally:
item_price.delete()
pos_inv.delete()
pr.delete()
def create_pos_invoice(**args): def create_pos_invoice(**args):

View File

@@ -84,12 +84,20 @@ class POSInvoiceMergeLog(Document):
sales_invoice.set_posting_time = 1 sales_invoice.set_posting_time = 1
sales_invoice.posting_date = getdate(self.posting_date) sales_invoice.posting_date = getdate(self.posting_date)
sales_invoice.save() sales_invoice.save()
self.write_off_fractional_amount(sales_invoice, data)
sales_invoice.submit() sales_invoice.submit()
self.consolidated_invoice = sales_invoice.name self.consolidated_invoice = sales_invoice.name
return sales_invoice.name return sales_invoice.name
def write_off_fractional_amount(self, invoice, data):
pos_invoice_grand_total = sum(d.grand_total for d in data)
if abs(pos_invoice_grand_total - invoice.grand_total) < 1:
invoice.write_off_amount += -1 * (pos_invoice_grand_total - invoice.grand_total)
invoice.save()
def process_merging_into_credit_note(self, data): def process_merging_into_credit_note(self, data):
credit_note = self.get_new_sales_invoice() credit_note = self.get_new_sales_invoice()
credit_note.is_return = 1 credit_note.is_return = 1
@@ -102,6 +110,7 @@ class POSInvoiceMergeLog(Document):
# TODO: return could be against multiple sales invoice which could also have been consolidated? # TODO: return could be against multiple sales invoice which could also have been consolidated?
# credit_note.return_against = self.consolidated_invoice # credit_note.return_against = self.consolidated_invoice
credit_note.save() credit_note.save()
self.write_off_fractional_amount(credit_note, data)
credit_note.submit() credit_note.submit()
self.consolidated_credit_note = credit_note.name self.consolidated_credit_note = credit_note.name
@@ -135,9 +144,15 @@ class POSInvoiceMergeLog(Document):
i.uom == item.uom and i.net_rate == item.net_rate and i.warehouse == item.warehouse): i.uom == item.uom and i.net_rate == item.net_rate and i.warehouse == item.warehouse):
found = True found = True
i.qty = i.qty + item.qty i.qty = i.qty + item.qty
i.amount = i.amount + item.net_amount
i.net_amount = i.amount
i.base_amount = i.base_amount + item.base_net_amount
i.base_net_amount = i.base_amount
if not found: if not found:
item.rate = item.net_rate item.rate = item.net_rate
item.amount = item.net_amount
item.base_amount = item.base_net_amount
item.price_list_rate = 0 item.price_list_rate = 0
si_item = map_child_doc(item, invoice, {"doctype": "Sales Invoice Item"}) si_item = map_child_doc(item, invoice, {"doctype": "Sales Invoice Item"})
items.append(si_item) items.append(si_item)
@@ -169,6 +184,7 @@ class POSInvoiceMergeLog(Document):
found = True found = True
if not found: if not found:
payments.append(payment) payments.append(payment)
rounding_adjustment += doc.rounding_adjustment rounding_adjustment += doc.rounding_adjustment
rounded_total += doc.rounded_total rounded_total += doc.rounded_total
base_rounding_adjustment += doc.base_rounding_adjustment base_rounding_adjustment += doc.base_rounding_adjustment

View File

@@ -12,6 +12,7 @@ from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_inv
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import ( from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import (
consolidate_pos_invoices, consolidate_pos_invoices,
) )
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
class TestPOSInvoiceMergeLog(unittest.TestCase): class TestPOSInvoiceMergeLog(unittest.TestCase):
@@ -150,3 +151,132 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
frappe.set_user("Administrator") frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`") frappe.db.sql("delete from `tabPOS Profile`")
frappe.db.sql("delete from `tabPOS Invoice`") frappe.db.sql("delete from `tabPOS Invoice`")
def test_consolidation_round_off_error_1(self):
'''
Test round off error in consolidated invoice creation if POS Invoice has inclusive tax
'''
frappe.db.sql("delete from `tabPOS Invoice`")
try:
make_stock_entry(
to_warehouse="_Test Warehouse - _TC",
item_code="_Test Item",
rate=8000,
qty=10,
)
init_user_and_profile()
inv = create_pos_invoice(qty=3, rate=10000, do_not_save=True)
inv.append("taxes", {
"account_head": "_Test Account VAT - _TC",
"charge_type": "On Net Total",
"cost_center": "_Test Cost Center - _TC",
"description": "VAT",
"doctype": "Sales Taxes and Charges",
"rate": 7.5,
"included_in_print_rate": 1
})
inv.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 30000
})
inv.insert()
inv.submit()
inv2 = create_pos_invoice(qty=3, rate=10000, do_not_save=True)
inv2.append("taxes", {
"account_head": "_Test Account VAT - _TC",
"charge_type": "On Net Total",
"cost_center": "_Test Cost Center - _TC",
"description": "VAT",
"doctype": "Sales Taxes and Charges",
"rate": 7.5,
"included_in_print_rate": 1
})
inv2.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 30000
})
inv2.insert()
inv2.submit()
consolidate_pos_invoices()
inv.load_from_db()
consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice)
self.assertEqual(consolidated_invoice.outstanding_amount, 0)
self.assertEqual(consolidated_invoice.status, 'Paid')
finally:
frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`")
frappe.db.sql("delete from `tabPOS Invoice`")
def test_consolidation_round_off_error_2(self):
'''
Test the same case as above but with an Unpaid POS Invoice
'''
frappe.db.sql("delete from `tabPOS Invoice`")
try:
make_stock_entry(
to_warehouse="_Test Warehouse - _TC",
item_code="_Test Item",
rate=8000,
qty=10,
)
init_user_and_profile()
inv = create_pos_invoice(qty=6, rate=10000, do_not_save=True)
inv.append("taxes", {
"account_head": "_Test Account VAT - _TC",
"charge_type": "On Net Total",
"cost_center": "_Test Cost Center - _TC",
"description": "VAT",
"doctype": "Sales Taxes and Charges",
"rate": 7.5,
"included_in_print_rate": 1
})
inv.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60000
})
inv.insert()
inv.submit()
inv2 = create_pos_invoice(qty=6, rate=10000, do_not_save=True)
inv2.append("taxes", {
"account_head": "_Test Account VAT - _TC",
"charge_type": "On Net Total",
"cost_center": "_Test Cost Center - _TC",
"description": "VAT",
"doctype": "Sales Taxes and Charges",
"rate": 7.5,
"included_in_print_rate": 1
})
inv2.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60000
})
inv2.insert()
inv2.submit()
inv3 = create_pos_invoice(qty=3, rate=600, do_not_save=True)
inv3.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 1000
})
inv3.insert()
inv3.submit()
consolidate_pos_invoices()
inv.load_from_db()
consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice)
self.assertEqual(consolidated_invoice.outstanding_amount, 800)
self.assertNotEqual(consolidated_invoice.status, 'Paid')
finally:
frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`")
frappe.db.sql("delete from `tabPOS Invoice`")

View File

@@ -249,13 +249,17 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
"free_item_data": [], "free_item_data": [],
"parent": args.parent, "parent": args.parent,
"parenttype": args.parenttype, "parenttype": args.parenttype,
"child_docname": args.get('child_docname') "child_docname": args.get('child_docname'),
}) })
if args.ignore_pricing_rule or not args.item_code: if args.ignore_pricing_rule or not args.item_code:
if frappe.db.exists(args.doctype, args.name) and args.get("pricing_rules"): if frappe.db.exists(args.doctype, args.name) and args.get("pricing_rules"):
item_details = remove_pricing_rule_for_item(args.get("pricing_rules"), item_details = remove_pricing_rule_for_item(
item_details, args.get('item_code')) args.get("pricing_rules"),
item_details,
item_code=args.get("item_code"),
rate=args.get("price_list_rate"),
)
return item_details return item_details
update_args_for_pricing_rule(args) update_args_for_pricing_rule(args)
@@ -308,8 +312,12 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
if not doc: return item_details if not doc: return item_details
elif args.get("pricing_rules"): elif args.get("pricing_rules"):
item_details = remove_pricing_rule_for_item(args.get("pricing_rules"), item_details = remove_pricing_rule_for_item(
item_details, args.get('item_code')) args.get("pricing_rules"),
item_details,
item_code=args.get("item_code"),
rate=args.get("price_list_rate"),
)
return item_details return item_details
@@ -390,7 +398,7 @@ def apply_price_discount_rule(pricing_rule, item_details, args):
item_details[field] += (pricing_rule.get(field, 0) item_details[field] += (pricing_rule.get(field, 0)
if pricing_rule else args.get(field, 0)) if pricing_rule else args.get(field, 0))
def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None): def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None, rate=None):
from erpnext.accounts.doctype.pricing_rule.utils import ( from erpnext.accounts.doctype.pricing_rule.utils import (
get_applied_pricing_rules, get_applied_pricing_rules,
get_pricing_rule_items, get_pricing_rule_items,
@@ -403,6 +411,7 @@ def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None):
if pricing_rule.rate_or_discount == 'Discount Percentage': if pricing_rule.rate_or_discount == 'Discount Percentage':
item_details.discount_percentage = 0.0 item_details.discount_percentage = 0.0
item_details.discount_amount = 0.0 item_details.discount_amount = 0.0
item_details.rate = rate or 0.0
if pricing_rule.rate_or_discount == 'Discount Amount': if pricing_rule.rate_or_discount == 'Discount Amount':
item_details.discount_amount = 0.0 item_details.discount_amount = 0.0
@@ -421,6 +430,7 @@ def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None):
item_details.applied_on_items = ','.join(items) item_details.applied_on_items = ','.join(items)
item_details.pricing_rules = '' item_details.pricing_rules = ''
item_details.pricing_rule_removed = True
return item_details return item_details
@@ -432,9 +442,12 @@ def remove_pricing_rules(item_list):
out = [] out = []
for item in item_list: for item in item_list:
item = frappe._dict(item) item = frappe._dict(item)
if item.get('pricing_rules'): if item.get("pricing_rules"):
out.append(remove_pricing_rule_for_item(item.get("pricing_rules"), out.append(
item, item.item_code)) remove_pricing_rule_for_item(
item.get("pricing_rules"), item, item.item_code, item.get("price_list_rate")
)
)
return out return out

View File

@@ -628,6 +628,67 @@ class TestPricingRule(unittest.TestCase):
for doc in [si, si1]: for doc in [si, si1]:
doc.delete() doc.delete()
def test_remove_pricing_rule(self):
item = make_item("Water Flask")
make_item_price("Water Flask", "_Test Price List", 100)
pricing_rule_record = {
"doctype": "Pricing Rule",
"title": "_Test Water Flask Rule",
"apply_on": "Item Code",
"price_or_product_discount": "Price",
"items": [{
"item_code": "Water Flask",
}],
"selling": 1,
"currency": "INR",
"rate_or_discount": "Discount Percentage",
"discount_percentage": 20,
"company": "_Test Company"
}
rule = frappe.get_doc(pricing_rule_record)
rule.insert()
si = create_sales_invoice(do_not_save=True, item_code="Water Flask")
si.selling_price_list = "_Test Price List"
si.save()
self.assertEqual(si.items[0].price_list_rate, 100)
self.assertEqual(si.items[0].discount_percentage, 20)
self.assertEqual(si.items[0].rate, 80)
si.ignore_pricing_rule = 1
si.save()
self.assertEqual(si.items[0].discount_percentage, 0)
self.assertEqual(si.items[0].rate, 100)
si.delete()
rule.delete()
frappe.get_doc("Item Price", {"item_code": "Water Flask"}).delete()
item.delete()
def test_multiple_pricing_rules_with_min_qty(self):
make_pricing_rule(discount_percentage=20, selling=1, priority=1, min_qty=4,
apply_multiple_pricing_rules=1, title="_Test Pricing Rule with Min Qty - 1")
make_pricing_rule(discount_percentage=10, selling=1, priority=2, min_qty=4,
apply_multiple_pricing_rules=1, title="_Test Pricing Rule with Min Qty - 2")
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1, currency="USD")
item = si.items[0]
item.stock_qty = 1
si.save()
self.assertFalse(item.discount_percentage)
item.qty = 5
item.stock_qty = 5
si.save()
self.assertEqual(item.discount_percentage, 30)
si.delete()
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 1")
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 2")
test_dependencies = ["Campaign"] test_dependencies = ["Campaign"]
def make_pricing_rule(**args): def make_pricing_rule(**args):

View File

@@ -73,7 +73,7 @@ def sorted_by_priority(pricing_rules, args, doc=None):
for key in sorted(pricing_rule_dict): for key in sorted(pricing_rule_dict):
pricing_rules_list.extend(pricing_rule_dict.get(key)) pricing_rules_list.extend(pricing_rule_dict.get(key))
return pricing_rules_list or pricing_rules return pricing_rules_list
def filter_pricing_rule_based_on_condition(pricing_rules, doc=None): def filter_pricing_rule_based_on_condition(pricing_rules, doc=None):
filtered_pricing_rules = [] filtered_pricing_rules = []

View File

@@ -178,8 +178,8 @@ class PurchaseInvoice(BuyingController):
if self.supplier and account.account_type != "Payable": if self.supplier and account.account_type != "Payable":
frappe.throw( frappe.throw(
_("Please ensure {} account is a Payable account. Change the account type to Payable or select a different account.") _("Please ensure {} account {} is a Payable account. Change the account type to Payable or select a different account.")
.format(frappe.bold("Credit To")), title=_("Invalid Account") .format(frappe.bold("Credit To"), frappe.bold(self.credit_to)), title=_("Invalid Account")
) )
self.party_account_currency = account.account_currency self.party_account_currency = account.account_currency
@@ -537,8 +537,11 @@ class PurchaseInvoice(BuyingController):
voucher_wise_stock_value = {} voucher_wise_stock_value = {}
if self.update_stock: if self.update_stock:
for d in frappe.get_all('Stock Ledger Entry', stock_ledger_entries = frappe.get_all("Stock Ledger Entry",
fields = ["voucher_detail_no", "stock_value_difference", "warehouse"], filters={'voucher_no': self.name}): fields = ["voucher_detail_no", "stock_value_difference", "warehouse"],
filters={"voucher_no": self.name, "voucher_type": self.doctype, "is_cancelled": 0}
)
for d in stock_ledger_entries:
voucher_wise_stock_value.setdefault((d.voucher_detail_no, d.warehouse), d.stock_value_difference) voucher_wise_stock_value.setdefault((d.voucher_detail_no, d.warehouse), d.stock_value_difference)
valuation_tax_accounts = [d.account_head for d in self.get("taxes") valuation_tax_accounts = [d.account_head for d in self.get("taxes")
@@ -548,6 +551,10 @@ class PurchaseInvoice(BuyingController):
exchange_rate_map, net_rate_map = get_purchase_document_details(self) exchange_rate_map, net_rate_map = get_purchase_document_details(self)
enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting')) enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting'))
provisional_accounting_for_non_stock_items = cint(frappe.db.get_value('Company', self.company, \
'enable_provisional_accounting_for_non_stock_items'))
purchase_receipt_doc_map = {}
for item in self.get("items"): for item in self.get("items"):
if flt(item.base_net_amount): if flt(item.base_net_amount):
@@ -643,19 +650,23 @@ class PurchaseInvoice(BuyingController):
else: else:
amount = flt(item.base_net_amount + item.item_tax_amount, item.precision("base_net_amount")) amount = flt(item.base_net_amount + item.item_tax_amount, item.precision("base_net_amount"))
auto_accounting_for_non_stock_items = cint(frappe.db.get_value('Company', self.company, 'enable_perpetual_inventory_for_non_stock_items')) if provisional_accounting_for_non_stock_items:
if auto_accounting_for_non_stock_items:
service_received_but_not_billed_account = self.get_company_default("service_received_but_not_billed")
if item.purchase_receipt: if item.purchase_receipt:
provisional_account = self.get_company_default("default_provisional_account")
purchase_receipt_doc = purchase_receipt_doc_map.get(item.purchase_receipt)
if not purchase_receipt_doc:
purchase_receipt_doc = frappe.get_doc("Purchase Receipt", item.purchase_receipt)
purchase_receipt_doc_map[item.purchase_receipt] = purchase_receipt_doc
# Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt # Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt
expense_booked_in_pr = frappe.db.get_value('GL Entry', {'is_cancelled': 0, expense_booked_in_pr = frappe.db.get_value('GL Entry', {'is_cancelled': 0,
'voucher_type': 'Purchase Receipt', 'voucher_no': item.purchase_receipt, 'voucher_detail_no': item.pr_detail, 'voucher_type': 'Purchase Receipt', 'voucher_no': item.purchase_receipt, 'voucher_detail_no': item.pr_detail,
'account':service_received_but_not_billed_account}, ['name']) 'account':provisional_account}, ['name'])
if expense_booked_in_pr: if expense_booked_in_pr:
expense_account = service_received_but_not_billed_account # Intentionally passing purchase invoice item to handle partial billing
purchase_receipt_doc.add_provisional_gl_entry(item, gl_entries, self.posting_date, reverse=1)
if not self.is_internal_transfer(): if not self.is_internal_transfer():
gl_entries.append(self.get_gl_dict({ gl_entries.append(self.get_gl_dict({

View File

@@ -56,4 +56,14 @@ frappe.listview_settings["Purchase Invoice"] = {
]; ];
} }
}, },
onload: function(listview) {
listview.page.add_action_item(__("Purchase Receipt"), ()=>{
erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Purchase Receipt");
});
listview.page.add_action_item(__("Payment"), ()=>{
erpnext.bulk_transaction_processing.create(listview, "Purchase Invoice", "Payment");
});
}
}; };

View File

@@ -11,12 +11,17 @@ from frappe.utils import add_days, cint, flt, getdate, nowdate, today
import erpnext import erpnext
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.buying.doctype.supplier.test_supplier import create_supplier from erpnext.buying.doctype.supplier.test_supplier import create_supplier
from erpnext.controllers.accounts_controller import get_payment_terms from erpnext.controllers.accounts_controller import get_payment_terms
from erpnext.controllers.buying_controller import QtyMismatchError from erpnext.controllers.buying_controller import QtyMismatchError
from erpnext.exceptions import InvalidCurrency from erpnext.exceptions import InvalidCurrency
from erpnext.projects.doctype.project.test_project import make_project from erpnext.projects.doctype.project.test_project import make_project
from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_purchase_invoice as create_purchase_invoice_from_receipt,
)
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import ( from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
get_taxes, get_taxes,
make_purchase_receipt, make_purchase_receipt,
@@ -1147,8 +1152,6 @@ class TestPurchaseInvoice(unittest.TestCase):
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
from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
# create a new supplier to test # create a new supplier to test
supplier = create_supplier(supplier_name = '_Test TDS Advance Supplier', supplier = create_supplier(supplier_name = '_Test TDS Advance Supplier',
@@ -1221,6 +1224,45 @@ class TestPurchaseInvoice(unittest.TestCase):
payment_entry.load_from_db() payment_entry.load_from_db()
self.assertEqual(payment_entry.taxes[0].allocated_amount, 0) self.assertEqual(payment_entry.taxes[0].allocated_amount, 0)
def test_provisional_accounting_entry(self):
item = create_item("_Test Non Stock Item", is_stock_item=0)
provisional_account = create_account(account_name="Provision Account",
parent_account="Current Liabilities - _TC", company="_Test Company")
company = frappe.get_doc('Company', '_Test Company')
company.enable_provisional_accounting_for_non_stock_items = 1
company.default_provisional_account = provisional_account
company.save()
pr = make_purchase_receipt(item_code="_Test Non Stock Item", posting_date=add_days(nowdate(), -2))
pi = create_purchase_invoice_from_receipt(pr.name)
pi.set_posting_time = 1
pi.posting_date = add_days(pr.posting_date, -1)
pi.items[0].expense_account = 'Cost of Goods Sold - _TC'
pi.save()
pi.submit()
# Check GLE for Purchase Invoice
expected_gle = [
['Cost of Goods Sold - _TC', 250, 0, add_days(pr.posting_date, -1)],
['Creditors - _TC', 0, 250, add_days(pr.posting_date, -1)]
]
check_gl_entries(self, pi.name, expected_gle, pi.posting_date)
expected_gle_for_purchase_receipt = [
["Provision Account - _TC", 250, 0, pr.posting_date],
["_Test Account Cost for Goods Sold - _TC", 0, 250, pr.posting_date],
["Provision Account - _TC", 0, 250, pi.posting_date],
["_Test Account Cost for Goods Sold - _TC", 250, 0, pi.posting_date]
]
check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date)
company.enable_provisional_accounting_for_non_stock_items = 0
company.save()
def check_gl_entries(doc, voucher_no, expected_gle, posting_date): def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
gl_entries = frappe.db.sql("""select account, debit, credit, posting_date gl_entries = frappe.db.sql("""select account, debit, credit, posting_date
from `tabGL Entry` from `tabGL Entry`

View File

@@ -1,6 +1,8 @@
{% include "erpnext/regional/india/taxes.js" %} {% include "erpnext/regional/india/taxes.js" %}
{% include "erpnext/regional/india/e_invoice/einvoice.js" %}
erpnext.setup_auto_gst_taxation('Sales Invoice'); erpnext.setup_auto_gst_taxation('Sales Invoice');
erpnext.setup_einvoice_actions('Sales Invoice')
frappe.ui.form.on("Sales Invoice", { frappe.ui.form.on("Sales Invoice", {
setup: function(frm) { setup: function(frm) {

View File

@@ -36,4 +36,139 @@ frappe.listview_settings['Sales Invoice'].onload = function (list_view) {
}; };
list_view.page.add_actions_menu_item(__('Generate E-Way Bill JSON'), action, false); list_view.page.add_actions_menu_item(__('Generate E-Way Bill JSON'), action, false);
const generate_irns = () => {
const docnames = list_view.get_checked_items(true);
if (docnames && docnames.length) {
frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.generate_einvoices',
args: { docnames },
freeze: true,
freeze_message: __('Generating E-Invoices...')
});
} else {
frappe.msgprint({
message: __('Please select at least one sales invoice to generate IRN'),
title: __('No Invoice Selected'),
indicator: 'red'
});
}
};
const cancel_irns = () => {
const docnames = list_view.get_checked_items(true);
const fields = [
{
"label": "Reason",
"fieldname": "reason",
"fieldtype": "Select",
"reqd": 1,
"default": "1-Duplicate",
"options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"]
},
{
"label": "Remark",
"fieldname": "remark",
"fieldtype": "Data",
"reqd": 1
}
];
const d = new frappe.ui.Dialog({
title: __("Cancel IRN"),
fields: fields,
primary_action: function() {
const data = d.get_values();
frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.cancel_irns',
args: {
doctype: list_view.doctype,
docnames,
reason: data.reason.split('-')[0],
remark: data.remark
},
freeze: true,
freeze_message: __('Cancelling E-Invoices...'),
});
d.hide();
},
primary_action_label: __('Submit')
});
d.show();
};
let einvoicing_enabled = false;
frappe.db.get_single_value("E Invoice Settings", "enable").then(enabled => {
einvoicing_enabled = enabled;
});
list_view.$result.on("change", "input[type=checkbox]", () => {
if (einvoicing_enabled) {
const docnames = list_view.get_checked_items(true);
// show/hide e-invoicing actions when no sales invoices are checked
if (docnames && docnames.length) {
// prevent adding actions twice if e-invoicing action group already exists
if (list_view.page.get_inner_group_button(__('E-Invoicing')).length == 0) {
list_view.page.add_inner_button(__('Generate IRNs'), generate_irns, __('E-Invoicing'));
list_view.page.add_inner_button(__('Cancel IRNs'), cancel_irns, __('E-Invoicing'));
}
} else {
list_view.page.remove_inner_button(__('Generate IRNs'), __('E-Invoicing'));
list_view.page.remove_inner_button(__('Cancel IRNs'), __('E-Invoicing'));
}
}
});
frappe.realtime.on("bulk_einvoice_generation_complete", (data) => {
const { failures, user, invoices } = data;
if (invoices.length != failures.length) {
frappe.msgprint({
message: __('{0} e-invoices generated successfully', [invoices.length]),
title: __('Bulk E-Invoice Generation Complete'),
indicator: 'orange'
});
}
if (failures && failures.length && user == frappe.session.user) {
let message = `
Failed to generate IRNs for following ${failures.length} sales invoices:
<ul style="padding-left: 20px; padding-top: 5px;">
${failures.map(d => `<li>${d.docname}</li>`).join('')}
</ul>
`;
frappe.msgprint({
message: message,
title: __('Bulk E-Invoice Generation Complete'),
indicator: 'orange'
});
}
});
frappe.realtime.on("bulk_einvoice_cancellation_complete", (data) => {
const { failures, user, invoices } = data;
if (invoices.length != failures.length) {
frappe.msgprint({
message: __('{0} e-invoices cancelled successfully', [invoices.length]),
title: __('Bulk E-Invoice Cancellation Complete'),
indicator: 'orange'
});
}
if (failures && failures.length && user == frappe.session.user) {
let message = `
Failed to cancel IRNs for following ${failures.length} sales invoices:
<ul style="padding-left: 20px; padding-top: 5px;">
${failures.map(d => `<li>${d.docname}</li>`).join('')}
</ul>
`;
frappe.msgprint({
message: message,
title: __('Bulk E-Invoice Cancellation Complete'),
indicator: 'orange'
});
}
});
}; };

View File

@@ -469,7 +469,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
let row = frappe.get_doc(d.doctype, d.name) let row = frappe.get_doc(d.doctype, d.name)
set_timesheet_detail_rate(row.doctype, row.name, me.frm.doc.currency, row.timesheet_detail) set_timesheet_detail_rate(row.doctype, row.name, me.frm.doc.currency, row.timesheet_detail)
}); });
frm.trigger("calculate_timesheet_totals"); this.frm.trigger("calculate_timesheet_totals");
} }
} }
}; };

View File

@@ -285,7 +285,7 @@ class SalesInvoice(SellingController):
filters={ invoice_or_credit_note: self.name }, filters={ invoice_or_credit_note: self.name },
pluck="pos_closing_entry" pluck="pos_closing_entry"
) )
if pos_closing_entry: if pos_closing_entry and pos_closing_entry[0]:
msg = _("To cancel a {} you need to cancel the POS Closing Entry {}.").format( msg = _("To cancel a {} you need to cancel the POS Closing Entry {}.").format(
frappe.bold("Consolidated Sales Invoice"), frappe.bold("Consolidated Sales Invoice"),
get_link_to_form("POS Closing Entry", pos_closing_entry[0]) get_link_to_form("POS Closing Entry", pos_closing_entry[0])
@@ -294,6 +294,8 @@ class SalesInvoice(SellingController):
def before_cancel(self): def before_cancel(self):
self.check_if_consolidated_invoice() self.check_if_consolidated_invoice()
super(SalesInvoice, self).before_cancel()
self.update_time_sheet(None) self.update_time_sheet(None)
def on_cancel(self): def on_cancel(self):
@@ -570,7 +572,10 @@ class SalesInvoice(SellingController):
frappe.throw(msg, title=_("Invalid Account")) frappe.throw(msg, title=_("Invalid Account"))
if self.customer and account.account_type != "Receivable": if self.customer and account.account_type != "Receivable":
msg = _("Please ensure {} account is a Receivable account.").format(frappe.bold("Debit To")) + " " msg = _("Please ensure {} account {} is a Receivable account.").format(
frappe.bold("Debit To"),
frappe.bold(self.debit_to)
) + " "
msg += _("Change the account type to Receivable or select a different account.") msg += _("Change the account type to Receivable or select a different account.")
frappe.throw(msg, title=_("Invalid Account")) frappe.throw(msg, title=_("Invalid Account"))
@@ -1247,14 +1252,14 @@ class SalesInvoice(SellingController):
def update_billing_status_in_dn(self, update_modified=True): def update_billing_status_in_dn(self, update_modified=True):
updated_delivery_notes = [] updated_delivery_notes = []
for d in self.get("items"): for d in self.get("items"):
if d.dn_detail: if d.so_detail:
updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified)
elif d.dn_detail:
billed_amt = frappe.db.sql("""select sum(amount) from `tabSales Invoice Item` billed_amt = frappe.db.sql("""select sum(amount) from `tabSales Invoice Item`
where dn_detail=%s and docstatus=1""", d.dn_detail) where dn_detail=%s and docstatus=1""", d.dn_detail)
billed_amt = billed_amt and billed_amt[0][0] or 0 billed_amt = billed_amt and billed_amt[0][0] or 0
frappe.db.set_value("Delivery Note Item", d.dn_detail, "billed_amt", billed_amt, update_modified=update_modified) frappe.db.set_value("Delivery Note Item", d.dn_detail, "billed_amt", billed_amt, update_modified=update_modified)
updated_delivery_notes.append(d.delivery_note) updated_delivery_notes.append(d.delivery_note)
elif d.so_detail:
updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified)
for dn in set(updated_delivery_notes): for dn in set(updated_delivery_notes):
frappe.get_doc("Delivery Note", dn).update_billing_percentage(update_modified=update_modified) frappe.get_doc("Delivery Note", dn).update_billing_percentage(update_modified=update_modified)

View File

@@ -21,5 +21,15 @@ frappe.listview_settings['Sales Invoice'] = {
}; };
return [__(doc.status), status_colors[doc.status], "status,=,"+doc.status]; return [__(doc.status), status_colors[doc.status], "status,=,"+doc.status];
}, },
right_column: "grand_total" right_column: "grand_total",
onload: function(listview) {
listview.page.add_action_item(__("Delivery Note"), ()=>{
erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Delivery Note");
});
listview.page.add_action_item(__("Payment"), ()=>{
erpnext.bulk_transaction_processing.create(listview, "Sales Invoice", "Payment");
});
}
}; };

View File

@@ -2100,6 +2100,54 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(data['billLists'][0]['actualFromStateCode'],7) self.assertEqual(data['billLists'][0]['actualFromStateCode'],7)
self.assertEqual(data['billLists'][0]['fromStateCode'],27) self.assertEqual(data['billLists'][0]['fromStateCode'],27)
def test_einvoice_submission_without_irn(self):
# init
einvoice_settings = frappe.get_doc('E Invoice Settings')
einvoice_settings.enable = 1
einvoice_settings.applicable_from = nowdate()
einvoice_settings.append('credentials', {
'company': '_Test Company',
'gstin': '27AAECE4835E1ZR',
'username': 'test',
'password': 'test'
})
einvoice_settings.save()
country = frappe.flags.country
frappe.flags.country = 'India'
si = make_sales_invoice_for_ewaybill()
self.assertRaises(frappe.ValidationError, si.submit)
si.irn = 'test_irn'
si.submit()
# reset
einvoice_settings = frappe.get_doc('E Invoice Settings')
einvoice_settings.enable = 0
frappe.flags.country = country
def test_einvoice_json(self):
from erpnext.regional.india.e_invoice.utils import make_einvoice, validate_totals
si = get_sales_invoice_for_e_invoice()
si.discount_amount = 100
si.save()
einvoice = make_einvoice(si)
self.assertTrue(einvoice['EwbDtls'])
validate_totals(einvoice)
si.apply_discount_on = 'Net Total'
si.save()
einvoice = make_einvoice(si)
validate_totals(einvoice)
[d.set('included_in_print_rate', 1) for d in si.taxes]
si.save()
einvoice = make_einvoice(si)
validate_totals(einvoice)
def test_item_tax_net_range(self): def test_item_tax_net_range(self):
item = create_item("T Shirt") item = create_item("T Shirt")

View File

@@ -46,7 +46,7 @@ def valdiate_taxes_and_charges_template(doc):
for tax in doc.get("taxes"): for tax in doc.get("taxes"):
validate_taxes_and_charges(tax) validate_taxes_and_charges(tax)
validate_account_head(tax, doc) validate_account_head(tax.idx, tax.account_head, doc.company)
validate_cost_center(tax, doc) validate_cost_center(tax, doc)
validate_inclusive_tax(tax, doc) validate_inclusive_tax(tax, doc)

View File

@@ -71,7 +71,8 @@ class ShippingRule(Document):
if doc.currency != doc.company_currency: if doc.currency != doc.company_currency:
shipping_amount = flt(shipping_amount / doc.conversion_rate, 2) shipping_amount = flt(shipping_amount / doc.conversion_rate, 2)
self.add_shipping_rule_to_tax_table(doc, shipping_amount) if shipping_amount:
self.add_shipping_rule_to_tax_table(doc, shipping_amount)
def get_shipping_amount_from_rules(self, value): def get_shipping_amount_from_rules(self, value):
for condition in self.get("conditions"): for condition in self.get("conditions"):

View File

@@ -2,12 +2,13 @@
"actions": [], "actions": [],
"allow_rename": 1, "allow_rename": 1,
"autoname": "field:title", "autoname": "field:title",
"creation": "2018-11-22 23:38:39.668804", "creation": "2022-01-19 01:09:28.920486",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"title" "title",
"disabled"
], ],
"fields": [ "fields": [
{ {
@@ -18,14 +19,21 @@
"label": "Title", "label": "Title",
"reqd": 1, "reqd": 1,
"unique": 1 "unique": 1
},
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-03-03 11:50:38.748872", "modified": "2022-01-18 21:13:41.161017",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Tax Category", "name": "Tax Category",
"naming_rule": "By fieldname",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@@ -65,5 +73,6 @@
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -98,7 +98,7 @@ class TaxRule(Document):
def validate_use_for_shopping_cart(self): def validate_use_for_shopping_cart(self):
'''If shopping cart is enabled and no tax rule exists for shopping cart, enable this one''' '''If shopping cart is enabled and no tax rule exists for shopping cart, enable this one'''
if (not self.use_for_shopping_cart if (not self.use_for_shopping_cart
and cint(frappe.db.get_single_value('Shopping Cart Settings', 'enabled')) 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]})): and not frappe.db.get_value('Tax Rule', {'use_for_shopping_cart': 1, 'name': ['!=', self.name]})):
self.use_for_shopping_cart = 1 self.use_for_shopping_cart = 1

View File

@@ -2,6 +2,8 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
import copy
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.meta import get_field_precision from frappe.model.meta import get_field_precision
@@ -51,49 +53,57 @@ def validate_accounting_period(gl_map):
.format(frappe.bold(accounting_periods[0].name)), ClosedAccountingPeriod) .format(frappe.bold(accounting_periods[0].name)), ClosedAccountingPeriod)
def process_gl_map(gl_map, merge_entries=True, precision=None): def process_gl_map(gl_map, merge_entries=True, precision=None):
if not gl_map:
return []
gl_map = distribute_gl_based_on_cost_center_allocation(gl_map, precision)
if merge_entries: if merge_entries:
gl_map = merge_similar_entries(gl_map, precision) gl_map = merge_similar_entries(gl_map, precision)
for entry in gl_map:
# toggle debit, credit if negative entry
if flt(entry.debit) < 0:
entry.credit = flt(entry.credit) - flt(entry.debit)
entry.debit = 0.0
if flt(entry.debit_in_account_currency) < 0: gl_map = toggle_debit_credit_if_negative(gl_map)
entry.credit_in_account_currency = \
flt(entry.credit_in_account_currency) - flt(entry.debit_in_account_currency)
entry.debit_in_account_currency = 0.0
if flt(entry.credit) < 0:
entry.debit = flt(entry.debit) - flt(entry.credit)
entry.credit = 0.0
if flt(entry.credit_in_account_currency) < 0:
entry.debit_in_account_currency = \
flt(entry.debit_in_account_currency) - flt(entry.credit_in_account_currency)
entry.credit_in_account_currency = 0.0
update_net_values(entry)
return gl_map return gl_map
def update_net_values(entry): def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None):
# In some scenarios net value needs to be shown in the ledger cost_center_allocation = get_cost_center_allocation_data(gl_map[0]["company"], gl_map[0]["posting_date"])
# This method updates net values as debit or credit if not cost_center_allocation:
if entry.post_net_value and entry.debit and entry.credit: return gl_map
if entry.debit > entry.credit:
entry.debit = entry.debit - entry.credit
entry.debit_in_account_currency = entry.debit_in_account_currency \
- entry.credit_in_account_currency
entry.credit = 0
entry.credit_in_account_currency = 0
else:
entry.credit = entry.credit - entry.debit
entry.credit_in_account_currency = entry.credit_in_account_currency \
- entry.debit_in_account_currency
entry.debit = 0 new_gl_map = []
entry.debit_in_account_currency = 0 for d in gl_map:
cost_center = d.get("cost_center")
if cost_center and cost_center_allocation.get(cost_center):
for sub_cost_center, percentage in cost_center_allocation.get(cost_center, {}).items():
gle = copy.deepcopy(d)
gle.cost_center = sub_cost_center
for field in ("debit", "credit", "debit_in_account_currency", "credit_in_company_currency"):
gle[field] = flt(flt(d.get(field)) * percentage / 100, precision)
new_gl_map.append(gle)
else:
new_gl_map.append(d)
return new_gl_map
def get_cost_center_allocation_data(company, posting_date):
par = frappe.qb.DocType("Cost Center Allocation")
child = frappe.qb.DocType("Cost Center Allocation Percentage")
records = (
frappe.qb.from_(par).inner_join(child).on(par.name == child.parent)
.select(par.main_cost_center, child.cost_center, child.percentage)
.where(par.docstatus == 1)
.where(par.company == company)
.where(par.valid_from <= posting_date)
.orderby(par.valid_from, order=frappe.qb.desc)
).run(as_dict=True)
cc_allocation = frappe._dict()
for d in records:
cc_allocation.setdefault(d.main_cost_center, frappe._dict())\
.setdefault(d.cost_center, d.percentage)
return cc_allocation
def merge_similar_entries(gl_map, precision=None): def merge_similar_entries(gl_map, precision=None):
merged_gl_map = [] merged_gl_map = []
@@ -145,6 +155,49 @@ def check_if_in_list(gle, gl_map, dimensions=None):
if same_head: if same_head:
return e return e
def toggle_debit_credit_if_negative(gl_map):
for entry in gl_map:
# toggle debit, credit if negative entry
if flt(entry.debit) < 0:
entry.credit = flt(entry.credit) - flt(entry.debit)
entry.debit = 0.0
if flt(entry.debit_in_account_currency) < 0:
entry.credit_in_account_currency = \
flt(entry.credit_in_account_currency) - flt(entry.debit_in_account_currency)
entry.debit_in_account_currency = 0.0
if flt(entry.credit) < 0:
entry.debit = flt(entry.debit) - flt(entry.credit)
entry.credit = 0.0
if flt(entry.credit_in_account_currency) < 0:
entry.debit_in_account_currency = \
flt(entry.debit_in_account_currency) - flt(entry.credit_in_account_currency)
entry.credit_in_account_currency = 0.0
update_net_values(entry)
return gl_map
def update_net_values(entry):
# In some scenarios net value needs to be shown in the ledger
# This method updates net values as debit or credit
if entry.post_net_value and entry.debit and entry.credit:
if entry.debit > entry.credit:
entry.debit = entry.debit - entry.credit
entry.debit_in_account_currency = entry.debit_in_account_currency \
- entry.credit_in_account_currency
entry.credit = 0
entry.credit_in_account_currency = 0
else:
entry.credit = entry.credit - entry.debit
entry.credit_in_account_currency = entry.credit_in_account_currency \
- entry.debit_in_account_currency
entry.debit = 0
entry.debit_in_account_currency = 0
def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False): def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
if not from_repost: if not from_repost:
validate_cwip_accounts(gl_map) validate_cwip_accounts(gl_map)
@@ -266,13 +319,18 @@ def make_reverse_gl_entries(gl_entries=None, voucher_type=None, voucher_no=None,
""" """
if not gl_entries: if not gl_entries:
gl_entries = frappe.get_all("GL Entry", gl_entry = frappe.qb.DocType("GL Entry")
fields = ["*"], gl_entries = (frappe.qb.from_(
filters = { gl_entry
"voucher_type": voucher_type, ).select(
"voucher_no": voucher_no, '*'
"is_cancelled": 0 ).where(
}) gl_entry.voucher_type == voucher_type
).where(
gl_entry.voucher_no == voucher_no
).where(
gl_entry.is_cancelled == 0
).for_update()).run(as_dict=1)
if gl_entries: if gl_entries:
validate_accounting_period(gl_entries) validate_accounting_period(gl_entries)
@@ -280,23 +338,24 @@ def make_reverse_gl_entries(gl_entries=None, voucher_type=None, voucher_no=None,
set_as_cancel(gl_entries[0]['voucher_type'], gl_entries[0]['voucher_no']) set_as_cancel(gl_entries[0]['voucher_type'], gl_entries[0]['voucher_no'])
for entry in gl_entries: for entry in gl_entries:
entry['name'] = None new_gle = copy.deepcopy(entry)
debit = entry.get('debit', 0) new_gle['name'] = None
credit = entry.get('credit', 0) debit = new_gle.get('debit', 0)
credit = new_gle.get('credit', 0)
debit_in_account_currency = entry.get('debit_in_account_currency', 0) debit_in_account_currency = new_gle.get('debit_in_account_currency', 0)
credit_in_account_currency = entry.get('credit_in_account_currency', 0) credit_in_account_currency = new_gle.get('credit_in_account_currency', 0)
entry['debit'] = credit new_gle['debit'] = credit
entry['credit'] = debit new_gle['credit'] = debit
entry['debit_in_account_currency'] = credit_in_account_currency new_gle['debit_in_account_currency'] = credit_in_account_currency
entry['credit_in_account_currency'] = debit_in_account_currency new_gle['credit_in_account_currency'] = debit_in_account_currency
entry['remarks'] = "On cancellation of " + entry['voucher_no'] new_gle['remarks'] = "On cancellation of " + new_gle['voucher_no']
entry['is_cancelled'] = 1 new_gle['is_cancelled'] = 1
if entry['debit'] or entry['credit']: if new_gle['debit'] or new_gle['credit']:
make_entry(entry, adv_adj, "Yes") make_entry(new_gle, adv_adj, "Yes")
def check_freezing_date(posting_date, adv_adj=False): def check_freezing_date(posting_date, adv_adj=False):

View File

@@ -58,7 +58,7 @@ def _get_party_details(party=None, account=None, party_type="Customer", company=
frappe.throw(_("Not permitted for {0}").format(party), frappe.PermissionError) frappe.throw(_("Not permitted for {0}").format(party), frappe.PermissionError)
party = frappe.get_doc(party_type, party) party = frappe.get_doc(party_type, party)
currency = party.default_currency if party.get("default_currency") else get_company_currency(company) currency = party.get("default_currency") or currency or get_company_currency(company)
party_address, shipping_address = set_address_details(party_details, party, party_type, doctype, company, party_address, company_address, shipping_address) party_address, shipping_address = set_address_details(party_details, party, party_type, doctype, company, party_address, company_address, shipping_address)
set_contact_details(party_details, party, party_type) set_contact_details(party_details, party, party_type)
@@ -307,7 +307,7 @@ def validate_party_gle_currency(party_type, party, company, party_account_curren
.format(frappe.bold(party_type), frappe.bold(party), frappe.bold(existing_gle_currency), frappe.bold(company)), InvalidAccountCurrency) .format(frappe.bold(party_type), frappe.bold(party), frappe.bold(existing_gle_currency), frappe.bold(company)), InvalidAccountCurrency)
def validate_party_accounts(doc): def validate_party_accounts(doc):
from erpnext.controllers.accounts_controller import validate_account_head
companies = [] companies = []
for account in doc.get("accounts"): for account in doc.get("accounts"):
@@ -330,6 +330,9 @@ def validate_party_accounts(doc):
if doc.default_currency != party_account_currency and doc.default_currency != company_default_currency: if doc.default_currency != party_account_currency and doc.default_currency != company_default_currency:
frappe.throw(_("Billing currency must be equal to either default company's currency or party account currency")) frappe.throw(_("Billing currency must be equal to either default company's currency or party account currency"))
# validate if account is mapped for same company
validate_account_head(account.idx, account.account, account.company)
@frappe.whitelist() @frappe.whitelist()
def get_due_date(posting_date, party_type, party, company=None, bill_date=None): def get_due_date(posting_date, party_type, party, company=None, bill_date=None):

View File

@@ -0,0 +1,173 @@
{%- from "templates/print_formats/standard_macros.html" import add_header, render_field, print_value -%}
{%- set einvoice = json.loads(doc.signed_einvoice) -%}
<div class="page-break">
<div {% if print_settings.repeat_header_footer %} id="header-html" class="hidden-pdf" {% endif %}>
{% if letter_head and not no_letterhead %}
<div class="letter-head">{{ letter_head }}</div>
{% endif %}
<div class="print-heading">
<h2>E Invoice<br><small>{{ doc.name }}</small></h2>
</div>
</div>
{% if print_settings.repeat_header_footer %}
<div id="footer-html" class="visible-pdf">
{% if not no_letterhead and footer %}
<div class="letter-head-footer">
{{ footer }}
</div>
{% endif %}
<p class="text-center small page-number visible-pdf">
{{ _("Page {0} of {1}").format('<span class="page"></span>', '<span class="topage"></span>') }}
</p>
</div>
{% endif %}
<h5 class="font-bold" style="margin-top: 0px;">1. Transaction Details</h5>
<div class="row section-break" style="border-bottom: 1px solid #d1d8dd; padding-bottom: 10px;">
<div class="col-xs-8 column-break">
<div class="row data-field">
<div class="col-xs-4"><label>IRN</label></div>
<div class="col-xs-8 value">{{ einvoice.Irn }}</div>
</div>
<div class="row data-field">
<div class="col-xs-4"><label>Ack. No</label></div>
<div class="col-xs-8 value">{{ einvoice.AckNo }}</div>
</div>
<div class="row data-field">
<div class="col-xs-4"><label>Ack. Date</label></div>
<div class="col-xs-8 value">{{ frappe.utils.format_datetime(einvoice.AckDt, "dd/MM/yyyy hh:mm:ss") }}</div>
</div>
<div class="row data-field">
<div class="col-xs-4"><label>Category</label></div>
<div class="col-xs-8 value">{{ einvoice.TranDtls.SupTyp }}</div>
</div>
<div class="row data-field">
<div class="col-xs-4"><label>Document Type</label></div>
<div class="col-xs-8 value">{{ einvoice.DocDtls.Typ }}</div>
</div>
<div class="row data-field">
<div class="col-xs-4"><label>Document No</label></div>
<div class="col-xs-8 value">{{ einvoice.DocDtls.No }}</div>
</div>
</div>
<div class="col-xs-4 column-break">
<img src="{{ doc.qrcode_image }}" width="175px" style="float: right;">
</div>
</div>
<h5 class="font-bold" style="margin-top: 15px; margin-bottom: 10px;">2. Party Details</h5>
<div class="row section-break" style="border-bottom: 1px solid #d1d8dd; padding-bottom: 10px;">
{%- set seller = einvoice.SellerDtls -%}
<div class="col-xs-6 column-break">
<h5 style="margin-bottom: 5px;">Seller</h5>
<p>{{ seller.Gstin }}</p>
<p>{{ seller.LglNm }}</p>
<p>{{ seller.Addr1 }}</p>
{%- if seller.Addr2 -%} <p>{{ seller.Addr2 }}</p> {% endif %}
<p>{{ seller.Loc }}</p>
<p>{{ frappe.db.get_value("Address", doc.company_address, "gst_state") }} - {{ seller.Pin }}</p>
{%- if einvoice.ShipDtls -%}
{%- set shipping = einvoice.ShipDtls -%}
<h5 style="margin-bottom: 5px;">Shipped From</h5>
<p>{{ shipping.Gstin }}</p>
<p>{{ shipping.LglNm }}</p>
<p>{{ shipping.Addr1 }}</p>
{%- if shipping.Addr2 -%} <p>{{ shipping.Addr2 }}</p> {% endif %}
<p>{{ shipping.Loc }}</p>
<p>{{ frappe.db.get_value("Address", doc.shipping_address_name, "gst_state") }} - {{ shipping.Pin }}</p>
{% endif %}
</div>
{%- set buyer = einvoice.BuyerDtls -%}
<div class="col-xs-6 column-break">
<h5 style="margin-bottom: 5px;">Buyer</h5>
<p>{{ buyer.Gstin }}</p>
<p>{{ buyer.LglNm }}</p>
<p>{{ buyer.Addr1 }}</p>
{%- if buyer.Addr2 -%} <p>{{ buyer.Addr2 }}</p> {% endif %}
<p>{{ buyer.Loc }}</p>
<p>{{ frappe.db.get_value("Address", doc.customer_address, "gst_state") }} - {{ buyer.Pin }}</p>
{%- if einvoice.DispDtls -%}
{%- set dispatch = einvoice.DispDtls -%}
<h5 style="margin-bottom: 5px;">Dispatched From</h5>
{%- if dispatch.Gstin -%} <p>{{ dispatch.Gstin }}</p> {% endif %}
<p>{{ dispatch.LglNm }}</p>
<p>{{ dispatch.Addr1 }}</p>
{%- if dispatch.Addr2 -%} <p>{{ dispatch.Addr2 }}</p> {% endif %}
<p>{{ dispatch.Loc }}</p>
<p>{{ frappe.db.get_value("Address", doc.dispatch_address_name, "gst_state") }} - {{ dispatch.Pin }}</p>
{% endif %}
</div>
</div>
<div style="overflow-x: auto;">
<h5 class="font-bold" style="margin-top: 15px; margin-bottom: 10px;">3. Item Details</h5>
<table class="table table-bordered">
<thead>
<tr>
<th class="text-left" style="width: 3%;">Sr. No.</th>
<th class="text-left">Item</th>
<th class="text-left" style="width: 10%;">HSN Code</th>
<th class="text-left" style="width: 5%;">Qty</th>
<th class="text-left" style="width: 5%;">UOM</th>
<th class="text-left">Rate</th>
<th class="text-left" style="width: 5%;">Discount</th>
<th class="text-left">Taxable Amount</th>
<th class="text-left" style="width: 7%;">Tax Rate</th>
<th class="text-left" style="width: 5%;">Other Charges</th>
<th class="text-left">Total</th>
</tr>
</thead>
<tbody>
{% for item in einvoice.ItemList %}
<tr>
<td class="text-left" style="width: 3%;">{{ item.SlNo }}</td>
<td class="text-left">{{ item.PrdDesc }}</td>
<td class="text-left" style="width: 10%;">{{ item.HsnCd }}</td>
<td class="text-right" style="width: 5%;">{{ item.Qty }}</td>
<td class="text-left" style="width: 5%;">{{ item.Unit }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(item.UnitPrice, None, "INR") }}</td>
<td class="text-right" style="width: 5%;">{{ frappe.utils.fmt_money(item.Discount, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(item.AssAmt, None, "INR") }}</td>
<td class="text-right" style="width: 7%;">{{ item.GstRt + item.CesRt }} %</td>
<td class="text-right" style="width: 5%;">{{ frappe.utils.fmt_money(0, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(item.TotItemVal, None, "INR") }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div style="overflow-x: auto;">
<h5 class="font-bold" style="margin-bottom: 0px;">4. Value Details</h5>
<table class="table table-bordered">
<thead>
<tr>
<th class="text-left">Taxable Amount</th>
<th class="text-left">CGST</th>
<th class="text-left"">SGST</th>
<th class="text-left">IGST</th>
<th class="text-left">CESS</th>
<th class="text-left" style="width: 10%;">State CESS</th>
<th class="text-left">Discount</th>
<th class="text-left" style="width: 10%;">Other Charges</th>
<th class="text-left" style="width: 10%;">Round Off</th>
<th class="text-left">Total Value</th>
</tr>
</thead>
<tbody>
{%- set value_details = einvoice.ValDtls -%}
<tr>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.AssVal, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.CgstVal, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.SgstVal, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.IgstVal, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.CesVal, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(0, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.Discount, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.OthChrg, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.RndOffAmt, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.TotInvVal, None, "INR") }}</td>
</tr>
</tbody>
</table>
</div>
</div>

View File

@@ -0,0 +1,24 @@
{
"align_labels_right": 1,
"creation": "2020-10-10 18:01:21.032914",
"custom_format": 0,
"default_print_language": "en-US",
"disabled": 1,
"doc_type": "Sales Invoice",
"docstatus": 0,
"doctype": "Print Format",
"font": "Default",
"html": "",
"idx": 0,
"line_breaks": 1,
"modified": "2020-10-23 19:54:40.634936",
"modified_by": "Administrator",
"module": "Accounts",
"name": "GST E-Invoice",
"owner": "Administrator",
"print_format_builder": 0,
"print_format_type": "Jinja",
"raw_printing": 0,
"show_section_headings": 1,
"standard": "Yes"
}

View File

@@ -1,23 +0,0 @@
{
"align_labels_right": 0,
"creation": "2017-08-08 12:33:04.773099",
"custom_format": 1,
"disabled": 0,
"doc_type": "Sales Invoice",
"docstatus": 0,
"doctype": "Print Format",
"font": "Default",
"html": "<style>\n\t.print-format table, .print-format tr, \n\t.print-format td, .print-format div, .print-format p {\n\t\tfont-family: Tahoma, sans-serif;\n\t\tline-height: 150%;\n\t\tvertical-align: middle;\n\t}\n\t@media screen {\n\t\t.print-format {\n\t\t\twidth: 4in;\n\t\t\tpadding: 0.25in;\n\t\t\tmin-height: 8in;\n\t\t}\n\t}\n</style>\n\n{% if letter_head %}\n {{ letter_head }}\n{% endif %}\n<p class=\"text-center\">\n\t{{ doc.company }}<br>\n\t{% if doc.company_address_display %}\n\t\t{% set company_address = doc.company_address_display.replace(\"\\n\", \" \").replace(\"<br>\", \" \") %}\n\t\t{% if \"GSTIN\" not in company_address %}\n\t\t\t{{ company_address }}\n\t\t\t<b>{{ _(\"GSTIN\") }}:</b>{{ doc.company_gstin }}\n\t\t{% else %}\n\t\t\t{{ company_address.replace(\"GSTIN\", \"<br>GSTIN\") }}\n\t\t{% endif %}\n\t{% endif %}\n\t<br>\n\t{% if doc.docstatus == 0 %}\n\t\t<b>{{ doc.status + \" \"+ (doc.select_print_heading or _(\"Invoice\")) }}</b><br>\n\t{% else %}\n\t\t<b>{{ doc.select_print_heading or _(\"Invoice\") }}</b><br>\n\t{% endif %}\n</p>\n<p>\n\t<b>{{ _(\"Receipt No\") }}:</b> {{ doc.name }}<br>\n\t<b>{{ _(\"Date\") }}:</b> {{ doc.get_formatted(\"posting_date\") }}<br>\n\t{% if doc.grand_total > 50000 %}\n\t\t{% set customer_address = doc.address_display.replace(\"\\n\", \" \").replace(\"<br>\", \" \") %}\n\t\t<b>{{ _(\"Customer\") }}:</b><br>\n\t\t{{ doc.customer_name }}<br>\n\t\t{{ customer_address }}\n\t{% endif %}\n</p>\n\n<hr>\n<table class=\"table table-condensed cart no-border\">\n\t<thead>\n\t\t<tr>\n\t\t\t<th width=\"50%\">{{ _(\"Item\") }}</b></th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Qty\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Amount\") }}</th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t{%- for item in doc.items -%}\n\t\t<tr>\n\t\t\t<td>\n\t\t\t\t{{ item.item_code }}\n\t\t\t\t{%- if item.item_name != item.item_code -%}\n\t\t\t\t\t<br>{{ item.item_name }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.gst_hsn_code -%}\n\t\t\t\t\t<br><b>{{ _(\"HSN/SAC\") }}:</b> {{ item.gst_hsn_code }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.serial_no -%}\n\t\t\t\t\t<br><b>{{ _(\"Serial No\") }}:</b> {{ item.serial_no }}\n\t\t\t\t{%- endif -%}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">{{ item.qty }}<br>@ {{ item.rate }}</td>\n\t\t\t<td class=\"text-right\">{{ item.get_formatted(\"amount\") }}</td>\n\t\t</tr>\n\t\t{%- endfor -%}\n\t</tbody>\n</table>\n<table class=\"table table-condensed no-border\">\n\t<tbody>\n\t\t<tr>\n\t\t\t{% if doc.flags.show_inclusive_tax_in_print %}\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t\t</td>\n\t\t\t{% else %}\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ _(\"Total\") }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"total\", doc) }}\n\t\t\t\t</td>\n\t\t\t{% endif %}\n\t\t</tr>\n\t\t{%- for row in doc.taxes -%}\n\t\t {%- if (not row.included_in_print_rate or doc.flags.show_inclusive_tax_in_print) and row.tax_amount != 0 -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ row.description }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ row.get_formatted(\"tax_amount\", doc) }}\n\t\t\t\t</td>\n\t\t\t<tr>\n\t\t {%- endif -%}\n\t\t{%- endfor -%}\n\t\t{%- if doc.discount_amount -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t{{ _(\"Discount\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"discount_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Grand Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.rounded_total -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Rounded Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"rounded_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Paid Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"paid_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t{%- if doc.change_amount -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Change Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"change_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t{%- endif -%}\n\t</tbody>\n</table>\n<p>{{ doc.terms or \"\" }}</p>\n<p class=\"text-center\">{{ _(\"Thank you, please visit again.\") }}</p>",
"idx": 0,
"line_breaks": 0,
"modified": "2020-04-29 16:39:12.936215",
"modified_by": "Administrator",
"module": "Accounts",
"name": "GST POS Invoice",
"owner": "Administrator",
"print_format_builder": 0,
"print_format_type": "Jinja",
"raw_printing": 0,
"show_section_headings": 0,
"standard": "Yes"
}

View File

@@ -5,7 +5,6 @@
import frappe import frappe
from frappe import _, scrub from frappe import _, scrub
from frappe.utils import cint, flt from frappe.utils import cint, flt
from six import iteritems
from erpnext.accounts.party import get_partywise_advanced_payment_amount from erpnext.accounts.party import get_partywise_advanced_payment_amount
from erpnext.accounts.report.accounts_receivable.accounts_receivable import ReceivablePayableReport from erpnext.accounts.report.accounts_receivable.accounts_receivable import ReceivablePayableReport
@@ -40,7 +39,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
if self.filters.show_gl_balance: if self.filters.show_gl_balance:
gl_balance_map = get_gl_balance(self.filters.report_date) gl_balance_map = get_gl_balance(self.filters.report_date)
for party, party_dict in iteritems(self.party_total): for party, party_dict in self.party_total.items():
if party_dict.outstanding == 0: if party_dict.outstanding == 0:
continue continue

View File

@@ -120,11 +120,11 @@ def check_opening_balance(asset, liability, equity):
opening_balance = 0 opening_balance = 0
float_precision = cint(frappe.db.get_default("float_precision")) or 2 float_precision = cint(frappe.db.get_default("float_precision")) or 2
if asset: if asset:
opening_balance = flt(asset[0].get("opening_balance", 0), float_precision) opening_balance = flt(asset[-1].get("opening_balance", 0), float_precision)
if liability: if liability:
opening_balance -= flt(liability[0].get("opening_balance", 0), float_precision) opening_balance -= flt(liability[-1].get("opening_balance", 0), float_precision)
if equity: if equity:
opening_balance -= flt(equity[0].get("opening_balance", 0), float_precision) opening_balance -= flt(equity[-1].get("opening_balance", 0), float_precision)
opening_balance = flt(opening_balance, float_precision) opening_balance = flt(opening_balance, float_precision)
if opening_balance: if opening_balance:

View File

@@ -4,7 +4,12 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import flt, getdate, nowdate from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import Sum
from frappe.utils import flt, getdate
from pypika import CustomFunction
from erpnext.accounts.utils import get_balance_on
def execute(filters=None): def execute(filters=None):
@@ -18,7 +23,6 @@ def execute(filters=None):
data = get_entries(filters) data = get_entries(filters)
from erpnext.accounts.utils import get_balance_on
balance_as_per_system = get_balance_on(filters["account"], filters["report_date"]) balance_as_per_system = get_balance_on(filters["account"], filters["report_date"])
total_debit, total_credit = 0,0 total_debit, total_credit = 0,0
@@ -118,7 +122,21 @@ def get_columns():
] ]
def get_entries(filters): def get_entries(filters):
journal_entries = frappe.db.sql(""" journal_entries = get_journal_entries(filters)
payment_entries = get_payment_entries(filters)
loan_entries = get_loan_entries(filters)
pos_entries = []
if filters.include_pos_transactions:
pos_entries = get_pos_entries(filters)
return sorted(list(payment_entries)+list(journal_entries+list(pos_entries) + list(loan_entries)),
key=lambda k: getdate(k['posting_date']))
def get_journal_entries(filters):
return frappe.db.sql("""
select "Journal Entry" as payment_document, jv.posting_date, select "Journal Entry" as payment_document, jv.posting_date,
jv.name as payment_entry, jvd.debit_in_account_currency as debit, jv.name as payment_entry, jvd.debit_in_account_currency as debit,
jvd.credit_in_account_currency as credit, jvd.against_account, jvd.credit_in_account_currency as credit, jvd.against_account,
@@ -130,7 +148,8 @@ def get_entries(filters):
and ifnull(jv.clearance_date, '4000-01-01') > %(report_date)s and ifnull(jv.clearance_date, '4000-01-01') > %(report_date)s
and ifnull(jv.is_opening, 'No') = 'No'""", filters, as_dict=1) and ifnull(jv.is_opening, 'No') = 'No'""", filters, as_dict=1)
payment_entries = frappe.db.sql(""" def get_payment_entries(filters):
return frappe.db.sql("""
select select
"Payment Entry" as payment_document, name as payment_entry, "Payment Entry" as payment_document, name as payment_entry,
reference_no, reference_date as ref_date, reference_no, reference_date as ref_date,
@@ -145,9 +164,8 @@ def get_entries(filters):
and ifnull(clearance_date, '4000-01-01') > %(report_date)s and ifnull(clearance_date, '4000-01-01') > %(report_date)s
""", filters, as_dict=1) """, filters, as_dict=1)
pos_entries = [] def get_pos_entries(filters):
if filters.include_pos_transactions: return frappe.db.sql("""
pos_entries = frappe.db.sql("""
select select
"Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit, "Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit,
si.posting_date, si.debit_to as against_account, sip.clearance_date, si.posting_date, si.debit_to as against_account, sip.clearance_date,
@@ -161,8 +179,42 @@ def get_entries(filters):
si.posting_date ASC, si.name DESC si.posting_date ASC, si.name DESC
""", filters, as_dict=1) """, filters, as_dict=1)
return sorted(list(payment_entries)+list(journal_entries+list(pos_entries)), def get_loan_entries(filters):
key=lambda k: k['posting_date'] or getdate(nowdate())) loan_docs = []
for doctype in ["Loan Disbursement", "Loan Repayment"]:
loan_doc = frappe.qb.DocType(doctype)
ifnull = CustomFunction('IFNULL', ['value', 'default'])
if doctype == "Loan Disbursement":
amount_field = (loan_doc.disbursed_amount).as_("credit")
posting_date = (loan_doc.disbursement_date).as_("posting_date")
account = loan_doc.disbursement_account
else:
amount_field = (loan_doc.amount_paid).as_("debit")
posting_date = (loan_doc.posting_date).as_("posting_date")
account = loan_doc.payment_account
entries = frappe.qb.from_(loan_doc).select(
ConstantColumn(doctype).as_("payment_document"),
(loan_doc.name).as_("payment_entry"),
(loan_doc.reference_number).as_("reference_no"),
(loan_doc.reference_date).as_("ref_date"),
amount_field,
posting_date,
).where(
loan_doc.docstatus == 1
).where(
account == filters.get('account')
).where(
posting_date <= getdate(filters.get('report_date'))
).where(
ifnull(loan_doc.clearance_date, '4000-01-01') > getdate(filters.get('report_date'))
).run(as_dict=1)
loan_docs.extend(entries)
return loan_docs
def get_amounts_not_reflected_in_system(filters): def get_amounts_not_reflected_in_system(filters):
je_amount = frappe.db.sql(""" je_amount = frappe.db.sql("""
@@ -182,7 +234,40 @@ def get_amounts_not_reflected_in_system(filters):
pe_amount = flt(pe_amount[0][0]) if pe_amount else 0.0 pe_amount = flt(pe_amount[0][0]) if pe_amount else 0.0
return je_amount + pe_amount loan_amount = get_loan_amount(filters)
return je_amount + pe_amount + loan_amount
def get_loan_amount(filters):
total_amount = 0
for doctype in ["Loan Disbursement", "Loan Repayment"]:
loan_doc = frappe.qb.DocType(doctype)
ifnull = CustomFunction('IFNULL', ['value', 'default'])
if doctype == "Loan Disbursement":
amount_field = Sum(loan_doc.disbursed_amount)
posting_date = (loan_doc.disbursement_date).as_("posting_date")
account = loan_doc.disbursement_account
else:
amount_field = Sum(loan_doc.amount_paid)
posting_date = (loan_doc.posting_date).as_("posting_date")
account = loan_doc.payment_account
amount = frappe.qb.from_(loan_doc).select(
amount_field
).where(
loan_doc.docstatus == 1
).where(
account == filters.get('account')
).where(
posting_date > getdate(filters.get('report_date'))
).where(
ifnull(loan_doc.clearance_date, '4000-01-01') <= getdate(filters.get('report_date'))
).run()[0][0]
total_amount += flt(amount)
return amount
def get_balance_row(label, amount, account_currency): def get_balance_row(label, amount, account_currency):
if amount > 0: if amount > 0:

View File

@@ -29,18 +29,6 @@ def execute(filters=None):
dimension_items = cam_map.get(dimension) dimension_items = cam_map.get(dimension)
if dimension_items: if dimension_items:
data = get_final_data(dimension, dimension_items, filters, period_month_ranges, data, 0) data = get_final_data(dimension, dimension_items, filters, period_month_ranges, data, 0)
else:
DCC_allocation = frappe.db.sql('''SELECT parent, sum(percentage_allocation) as percentage_allocation
FROM `tabDistributed Cost Center`
WHERE cost_center IN %(dimension)s
AND parent NOT IN %(dimension)s
GROUP BY parent''',{'dimension':[dimension]})
if DCC_allocation:
filters['budget_against_filter'] = [DCC_allocation[0][0]]
ddc_cam_map = get_dimension_account_month_map(filters)
dimension_items = ddc_cam_map.get(DCC_allocation[0][0])
if dimension_items:
data = get_final_data(dimension, dimension_items, filters, period_month_ranges, data, DCC_allocation[0][1])
chart = get_chart_data(filters, columns, data) chart = get_chart_data(filters, columns, data)

View File

@@ -354,9 +354,6 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies):
if d.parent_account: if d.parent_account:
account = d.parent_account_name account = d.parent_account_name
# if not accounts_by_name.get(account):
# continue
for company in companies: for company in companies:
accounts_by_name[account][company] = \ accounts_by_name[account][company] = \
accounts_by_name[account].get(company, 0.0) + d.get(company, 0.0) accounts_by_name[account].get(company, 0.0) + d.get(company, 0.0)
@@ -367,7 +364,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies):
accounts_by_name[account].get("opening_balance", 0.0) + d.get("opening_balance", 0.0) accounts_by_name[account].get("opening_balance", 0.0) + d.get("opening_balance", 0.0)
def get_account_heads(root_type, companies, filters): def get_account_heads(root_type, companies, filters):
accounts = get_accounts(root_type, filters) accounts = get_accounts(root_type, companies)
if not accounts: if not accounts:
return None, None, None return None, None, None
@@ -396,7 +393,7 @@ def update_parent_account_names(accounts):
for account in accounts: for account in accounts:
if account.parent_account: if account.parent_account:
account["parent_account_name"] = name_to_account_map[account.parent_account] account["parent_account_name"] = name_to_account_map.get(account.parent_account)
return accounts return accounts
@@ -419,12 +416,19 @@ def get_subsidiary_companies(company):
return frappe.db.sql_list("""select name from `tabCompany` return frappe.db.sql_list("""select name from `tabCompany`
where lft >= {0} and rgt <= {1} order by lft, rgt""".format(lft, rgt)) where lft >= {0} and rgt <= {1} order by lft, rgt""".format(lft, rgt))
def get_accounts(root_type, filters): def get_accounts(root_type, companies):
return frappe.db.sql(""" select name, is_group, company, accounts = []
parent_account, lft, rgt, root_type, report_type, account_name, account_number added_accounts = []
from
`tabAccount` where company = %s and root_type = %s for company in companies:
""" , (filters.get('company'), root_type), as_dict=1) for account in frappe.get_all("Account", fields=["name", "is_group", "company",
"parent_account", "lft", "rgt", "root_type", "report_type", "account_name", "account_number"],
filters={"company": company, "root_type": root_type}):
if account.account_name not in added_accounts:
accounts.append(account)
added_accounts.append(account.account_name)
return accounts
def prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency, filters): def prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency, filters):
data = [] data = []

View File

@@ -282,7 +282,8 @@ def add_total_row(out, root_type, balance_must_be, period_list, company_currency
total_row = { total_row = {
"account_name": _("Total {0} ({1})").format(_(root_type), _(balance_must_be)), "account_name": _("Total {0} ({1})").format(_(root_type), _(balance_must_be)),
"account": _("Total {0} ({1})").format(_(root_type), _(balance_must_be)), "account": _("Total {0} ({1})").format(_(root_type), _(balance_must_be)),
"currency": company_currency "currency": company_currency,
"opening_balance": 0.0
} }
for row in out: for row in out:
@@ -294,6 +295,7 @@ def add_total_row(out, root_type, balance_must_be, period_list, company_currency
total_row.setdefault("total", 0.0) total_row.setdefault("total", 0.0)
total_row["total"] += flt(row["total"]) total_row["total"] += flt(row["total"])
total_row["opening_balance"] += row["opening_balance"]
row["total"] = "" row["total"] = ""
if "total" in total_row: if "total" in total_row:
@@ -387,42 +389,15 @@ def set_gl_entries_by_account(
key: value key: value
}) })
distributed_cost_center_query = "" gl_entries = frappe.db.sql("""
if filters and filters.get('cost_center'): select posting_date, account, debit, credit, is_opening, fiscal_year,
distributed_cost_center_query = """ debit_in_account_currency, credit_in_account_currency, account_currency from `tabGL Entry`
UNION ALL
SELECT posting_date,
account,
debit*(DCC_allocation.percentage_allocation/100) as debit,
credit*(DCC_allocation.percentage_allocation/100) as credit,
is_opening,
fiscal_year,
debit_in_account_currency*(DCC_allocation.percentage_allocation/100) as debit_in_account_currency,
credit_in_account_currency*(DCC_allocation.percentage_allocation/100) as credit_in_account_currency,
account_currency
FROM `tabGL Entry`,
(
SELECT parent, sum(percentage_allocation) as percentage_allocation
FROM `tabDistributed Cost Center`
WHERE cost_center IN %(cost_center)s
AND parent NOT IN %(cost_center)s
GROUP BY parent
) as DCC_allocation
WHERE company=%(company)s
{additional_conditions}
AND posting_date <= %(to_date)s
AND is_cancelled = 0
AND cost_center = DCC_allocation.parent
""".format(additional_conditions=additional_conditions.replace("and cost_center in %(cost_center)s ", ''))
gl_entries = frappe.db.sql("""select posting_date, account, debit, credit, is_opening, fiscal_year, debit_in_account_currency, credit_in_account_currency, account_currency from `tabGL Entry`
where company=%(company)s where company=%(company)s
{additional_conditions} {additional_conditions}
and posting_date <= %(to_date)s and posting_date <= %(to_date)s
and is_cancelled = 0 and is_cancelled = 0""".format(
{distributed_cost_center_query}""".format( additional_conditions=additional_conditions), gl_filters, as_dict=True
additional_conditions=additional_conditions, )
distributed_cost_center_query=distributed_cost_center_query), gl_filters, as_dict=True) #nosec
if filters and filters.get('presentation_currency'): if filters and filters.get('presentation_currency'):
convert_to_presentation_currency(gl_entries, get_currency(filters), filters.get('company')) convert_to_presentation_currency(gl_entries, get_currency(filters), filters.get('company'))

View File

@@ -176,44 +176,7 @@ def get_gl_entries(filters, accounting_dimensions):
if accounting_dimensions: if accounting_dimensions:
dimension_fields = ', '.join(accounting_dimensions) + ',' dimension_fields = ', '.join(accounting_dimensions) + ','
distributed_cost_center_query = "" gl_entries = frappe.db.sql("""
if filters and filters.get('cost_center'):
select_fields_with_percentage = """, debit*(DCC_allocation.percentage_allocation/100) as debit,
credit*(DCC_allocation.percentage_allocation/100) as credit,
debit_in_account_currency*(DCC_allocation.percentage_allocation/100) as debit_in_account_currency,
credit_in_account_currency*(DCC_allocation.percentage_allocation/100) as credit_in_account_currency """
distributed_cost_center_query = """
UNION ALL
SELECT name as gl_entry,
posting_date,
account,
party_type,
party,
voucher_type,
voucher_no, {dimension_fields}
cost_center, project,
against_voucher_type,
against_voucher,
account_currency,
remarks, against,
is_opening, `tabGL Entry`.creation {select_fields_with_percentage}
FROM `tabGL Entry`,
(
SELECT parent, sum(percentage_allocation) as percentage_allocation
FROM `tabDistributed Cost Center`
WHERE cost_center IN %(cost_center)s
AND parent NOT IN %(cost_center)s
GROUP BY parent
) as DCC_allocation
WHERE company=%(company)s
{conditions}
AND posting_date <= %(to_date)s
AND cost_center = DCC_allocation.parent
""".format(dimension_fields=dimension_fields,select_fields_with_percentage=select_fields_with_percentage, conditions=get_conditions(filters).replace("and cost_center in %(cost_center)s ", ''))
gl_entries = frappe.db.sql(
"""
select select
name as gl_entry, posting_date, account, party_type, party, name as gl_entry, posting_date, account, party_type, party,
voucher_type, voucher_no, {dimension_fields} voucher_type, voucher_no, {dimension_fields}
@@ -222,13 +185,11 @@ def get_gl_entries(filters, accounting_dimensions):
remarks, against, is_opening, creation {select_fields} remarks, against, is_opening, creation {select_fields}
from `tabGL Entry` from `tabGL Entry`
where company=%(company)s {conditions} where company=%(company)s {conditions}
{distributed_cost_center_query}
{order_by_statement} {order_by_statement}
""".format( """.format(
dimension_fields=dimension_fields, select_fields=select_fields, conditions=get_conditions(filters), distributed_cost_center_query=distributed_cost_center_query, dimension_fields=dimension_fields, select_fields=select_fields,
order_by_statement=order_by_statement conditions=get_conditions(filters), order_by_statement=order_by_statement
), ), filters, as_dict=1)
filters, as_dict=1)
if filters.get('presentation_currency'): if filters.get('presentation_currency'):
return convert_to_presentation_currency(gl_entries, currency_map, filters.get('company')) return convert_to_presentation_currency(gl_entries, currency_map, filters.get('company'))

View File

@@ -8,20 +8,22 @@ frappe.query_reports["Gross Profit"] = {
"label": __("Company"), "label": __("Company"),
"fieldtype": "Link", "fieldtype": "Link",
"options": "Company", "options": "Company",
"reqd": 1, "default": frappe.defaults.get_user_default("Company"),
"default": frappe.defaults.get_user_default("Company") "reqd": 1
}, },
{ {
"fieldname":"from_date", "fieldname":"from_date",
"label": __("From Date"), "label": __("From Date"),
"fieldtype": "Date", "fieldtype": "Date",
"default": frappe.defaults.get_user_default("year_start_date") "default": frappe.defaults.get_user_default("year_start_date"),
"reqd": 1
}, },
{ {
"fieldname":"to_date", "fieldname":"to_date",
"label": __("To Date"), "label": __("To Date"),
"fieldtype": "Date", "fieldtype": "Date",
"default": frappe.defaults.get_user_default("year_end_date") "default": frappe.defaults.get_user_default("year_end_date"),
"reqd": 1
}, },
{ {
"fieldname":"sales_invoice", "fieldname":"sales_invoice",
@@ -42,6 +44,11 @@ frappe.query_reports["Gross Profit"] = {
"parent_field": "parent_invoice", "parent_field": "parent_invoice",
"initial_depth": 3, "initial_depth": 3,
"formatter": function(value, row, column, data, default_formatter) { "formatter": function(value, row, column, data, default_formatter) {
if (column.fieldname == "sales_invoice" && column.options == "Item" && data.indent == 0) {
column._options = "Sales Invoice";
} else {
column._options = "Item";
}
value = default_formatter(value, row, column, data); value = default_formatter(value, row, column, data);
if (data && (data.indent == 0.0 || row[1].content == "Total")) { if (data && (data.indent == 0.0 || row[1].content == "Total")) {

View File

@@ -1,5 +1,5 @@
{ {
"add_total_row": 0, "add_total_row": 1,
"columns": [], "columns": [],
"creation": "2013-02-25 17:03:34", "creation": "2013-02-25 17:03:34",
"disable_prepared_report": 0, "disable_prepared_report": 0,
@@ -9,7 +9,7 @@
"filters": [], "filters": [],
"idx": 3, "idx": 3,
"is_standard": "Yes", "is_standard": "Yes",
"modified": "2021-11-13 19:14:23.730198", "modified": "2022-02-11 10:18:36.956558",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Gross Profit", "name": "Gross Profit",

View File

@@ -70,43 +70,42 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_
data.append(row) data.append(row)
def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_columns, data): def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_columns, data):
for idx, src in enumerate(gross_profit_data.grouped_data): for src in gross_profit_data.grouped_data:
row = [] row = []
for col in group_wise_columns.get(scrub(filters.group_by)): for col in group_wise_columns.get(scrub(filters.group_by)):
row.append(src.get(col)) row.append(src.get(col))
row.append(filters.currency) row.append(filters.currency)
if idx == len(gross_profit_data.grouped_data)-1:
row[0] = "Total"
data.append(row) data.append(row)
def get_columns(group_wise_columns, filters): def get_columns(group_wise_columns, filters):
columns = [] columns = []
column_map = frappe._dict({ column_map = frappe._dict({
"parent": _("Sales Invoice") + ":Link/Sales Invoice:120", "parent": {"label": _('Sales Invoice'), "fieldname": "parent_invoice", "fieldtype": "Link", "options": "Sales Invoice", "width": 120},
"invoice_or_item": _("Sales Invoice") + ":Link/Sales Invoice:120", "invoice_or_item": {"label": _('Sales Invoice'), "fieldtype": "Link", "options": "Sales Invoice", "width": 120},
"posting_date": _("Posting Date") + ":Date:100", "posting_date": {"label": _('Posting Date'), "fieldname": "posting_date", "fieldtype": "Date", "width": 100},
"posting_time": _("Posting Time") + ":Data:100", "posting_time": {"label": _('Posting Time'), "fieldname": "posting_time", "fieldtype": "Data", "width": 100},
"item_code": _("Item Code") + ":Link/Item:100", "item_code": {"label": _('Item Code'), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", "width": 100},
"item_name": _("Item Name") + ":Data:100", "item_name": {"label": _('Item Name'), "fieldname": "item_name", "fieldtype": "Data", "width": 100},
"item_group": _("Item Group") + ":Link/Item Group:100", "item_group": {"label": _('Item Group'), "fieldname": "item_group", "fieldtype": "Link", "options": "Item Group", "width": 100},
"brand": _("Brand") + ":Link/Brand:100", "brand": {"label": _('Brand'), "fieldtype": "Link", "options": "Brand", "width": 100},
"description": _("Description") +":Data:100", "description": {"label": _('Description'), "fieldname": "description", "fieldtype": "Data", "width": 100},
"warehouse": _("Warehouse") + ":Link/Warehouse:100", "warehouse": {"label": _('Warehouse'), "fieldname": "warehouse", "fieldtype": "Link", "options": "warehouse", "width": 100},
"qty": _("Qty") + ":Float:80", "qty": {"label": _('Qty'), "fieldname": "qty", "fieldtype": "Float", "width": 80},
"base_rate": _("Avg. Selling Rate") + ":Currency/currency:100", "base_rate": {"label": _('Avg. Selling Rate'), "fieldname": "avg._selling_rate", "fieldtype": "Currency", "options": "currency", "width": 100},
"buying_rate": _("Valuation Rate") + ":Currency/currency:100", "buying_rate": {"label": _('Valuation Rate'), "fieldname": "valuation_rate", "fieldtype": "Currency", "options": "currency", "width": 100},
"base_amount": _("Selling Amount") + ":Currency/currency:100", "base_amount": {"label": _('Selling Amount'), "fieldname": "selling_amount", "fieldtype": "Currency", "options": "currency", "width": 100},
"buying_amount": _("Buying Amount") + ":Currency/currency:100", "buying_amount": {"label": _('Buying Amount'), "fieldname": "buying_amount", "fieldtype": "Currency", "options": "currency", "width": 100},
"gross_profit": _("Gross Profit") + ":Currency/currency:100", "gross_profit": {"label": _('Gross Profit'), "fieldname": "gross_profit", "fieldtype": "Currency", "options": "currency", "width": 100},
"gross_profit_percent": _("Gross Profit %") + ":Percent:100", "gross_profit_percent": {"label": _('Gross Profit Percent'), "fieldname": "gross_profit_%",
"project": _("Project") + ":Link/Project:100", "fieldtype": "Percent", "width": 100},
"sales_person": _("Sales person"), "project": {"label": _('Project'), "fieldname": "project", "fieldtype": "Link", "options": "Project", "width": 100},
"allocated_amount": _("Allocated Amount") + ":Currency/currency:100", "sales_person": {"label": _('Sales Person'), "fieldname": "sales_person", "fieldtype": "Data","width": 100},
"customer": _("Customer") + ":Link/Customer:100", "allocated_amount": {"label": _('Allocated Amount'), "fieldname": "allocated_amount", "fieldtype": "Currency", "options": "currency", "width": 100},
"customer_group": _("Customer Group") + ":Link/Customer Group:100", "customer": {"label": _('Customer'), "fieldname": "customer", "fieldtype": "Link", "options": "Customer", "width": 100},
"territory": _("Territory") + ":Link/Territory:100" "customer_group": {"label": _('Customer Group'), "fieldname": "customer_group", "fieldtype": "Link", "options": "customer", "width": 100},
"territory": {"label": _('Territory'), "fieldname": "territory", "fieldtype": "Link", "options": "territory", "width": 100},
}) })
for col in group_wise_columns.get(scrub(filters.group_by)): for col in group_wise_columns.get(scrub(filters.group_by)):
@@ -173,7 +172,7 @@ class GrossProfitGenerator(object):
buying_amount = 0 buying_amount = 0
for row in reversed(self.si_list): for row in reversed(self.si_list):
if self.skip_row(row, self.product_bundles): if self.skip_row(row):
continue continue
row.base_amount = flt(row.base_net_amount, self.currency_precision) row.base_amount = flt(row.base_net_amount, self.currency_precision)
@@ -223,16 +222,6 @@ class GrossProfitGenerator(object):
self.get_average_rate_based_on_group_by() self.get_average_rate_based_on_group_by()
def get_average_rate_based_on_group_by(self): def get_average_rate_based_on_group_by(self):
# sum buying / selling totals for group
self.totals = frappe._dict(
qty=0,
base_amount=0,
buying_amount=0,
gross_profit=0,
gross_profit_percent=0,
base_rate=0,
buying_rate=0
)
for key in list(self.grouped): for key in list(self.grouped):
if self.filters.get("group_by") != "Invoice": if self.filters.get("group_by") != "Invoice":
for i, row in enumerate(self.grouped[key]): for i, row in enumerate(self.grouped[key]):
@@ -244,7 +233,6 @@ class GrossProfitGenerator(object):
new_row.base_amount += flt(row.base_amount, self.currency_precision) new_row.base_amount += flt(row.base_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)
self.add_to_totals(new_row)
else: else:
for i, row in enumerate(self.grouped[key]): for i, row in enumerate(self.grouped[key]):
if row.indent == 1.0: if row.indent == 1.0:
@@ -258,17 +246,6 @@ class GrossProfitGenerator(object):
if (flt(row.qty) or row.base_amount): if (flt(row.qty) or row.base_amount):
row = self.set_average_rate(row) row = self.set_average_rate(row)
self.grouped_data.append(row) self.grouped_data.append(row)
self.add_to_totals(row)
self.set_average_gross_profit(self.totals)
if self.filters.get("group_by") == "Invoice":
self.totals.indent = 0.0
self.totals.parent_invoice = ""
self.totals.invoice_or_item = "Total"
self.si_list.append(self.totals)
else:
self.grouped_data.append(self.totals)
def is_not_invoice_row(self, row): def is_not_invoice_row(self, row):
return (self.filters.get("group_by") == "Invoice" and row.indent != 0.0) or self.filters.get("group_by") != "Invoice" return (self.filters.get("group_by") == "Invoice" and row.indent != 0.0) or self.filters.get("group_by") != "Invoice"
@@ -284,11 +261,6 @@ class GrossProfitGenerator(object):
new_row.gross_profit_percent = flt(((new_row.gross_profit / new_row.base_amount) * 100.0), self.currency_precision) \ new_row.gross_profit_percent = flt(((new_row.gross_profit / new_row.base_amount) * 100.0), self.currency_precision) \
if new_row.base_amount else 0 if new_row.base_amount else 0
def add_to_totals(self, new_row):
for key in self.totals:
if new_row.get(key):
self.totals[key] += new_row[key]
def get_returned_invoice_items(self): def get_returned_invoice_items(self):
returned_invoices = frappe.db.sql(""" returned_invoices = frappe.db.sql("""
select select
@@ -306,12 +278,12 @@ class GrossProfitGenerator(object):
self.returned_invoices.setdefault(inv.return_against, frappe._dict())\ self.returned_invoices.setdefault(inv.return_against, frappe._dict())\
.setdefault(inv.item_code, []).append(inv) .setdefault(inv.item_code, []).append(inv)
def skip_row(self, row, product_bundles): def skip_row(self, row):
if self.filters.get("group_by") != "Invoice": if self.filters.get("group_by") != "Invoice":
if not row.get(scrub(self.filters.get("group_by", ""))): if not row.get(scrub(self.filters.get("group_by", ""))):
return True return True
elif row.get("is_return") == 1:
return True return False
def get_buying_amount_from_product_bundle(self, row, product_bundle): def get_buying_amount_from_product_bundle(self, row, product_bundle):
buying_amount = 0.0 buying_amount = 0.0
@@ -369,20 +341,37 @@ class GrossProfitGenerator(object):
return self.average_buying_rate[item_code] return self.average_buying_rate[item_code]
def get_last_purchase_rate(self, item_code, row): def get_last_purchase_rate(self, item_code, row):
condition = '' purchase_invoice = frappe.qb.DocType("Purchase Invoice")
if row.project: purchase_invoice_item = frappe.qb.DocType("Purchase Invoice Item")
condition += " AND a.project=%s" % (frappe.db.escape(row.project))
elif row.cost_center:
condition += " AND a.cost_center=%s" % (frappe.db.escape(row.cost_center))
if self.filters.to_date:
condition += " AND modified='%s'" % (self.filters.to_date)
last_purchase_rate = frappe.db.sql(""" query = (frappe.qb.from_(purchase_invoice_item)
select (a.base_rate / a.conversion_factor) .inner_join(
from `tabPurchase Invoice Item` a purchase_invoice
where a.item_code = %s and a.docstatus=1 ).on(
{0} purchase_invoice.name == purchase_invoice_item.parent
order by a.modified desc limit 1""".format(condition), item_code) ).select(
purchase_invoice_item.base_rate / purchase_invoice_item.conversion_factor
).where(
purchase_invoice.docstatus == 1
).where(
purchase_invoice.posting_date <= self.filters.to_date
).where(
purchase_invoice_item.item_code == item_code
))
if row.project:
query.where(
purchase_invoice_item.project == row.project
)
if row.cost_center:
query.where(
purchase_invoice_item.cost_center == row.cost_center
)
query.orderby(purchase_invoice.posting_date, order=frappe.qb.desc)
query.limit(1)
last_purchase_rate = query.run()
return flt(last_purchase_rate[0][0]) if last_purchase_rate else 0 return flt(last_purchase_rate[0][0]) if last_purchase_rate else 0

View File

@@ -109,7 +109,6 @@ def accumulate_values_into_parents(accounts, accounts_by_name):
def prepare_data(accounts, filters, total_row, parent_children_map, based_on): def prepare_data(accounts, filters, total_row, parent_children_map, based_on):
data = [] data = []
new_accounts = accounts
company_currency = frappe.get_cached_value('Company', filters.get("company"), "default_currency") company_currency = frappe.get_cached_value('Company', filters.get("company"), "default_currency")
for d in accounts: for d in accounts:
@@ -123,19 +122,6 @@ def prepare_data(accounts, filters, total_row, parent_children_map, based_on):
"currency": company_currency, "currency": company_currency,
"based_on": based_on "based_on": based_on
} }
if based_on == 'cost_center':
cost_center_doc = frappe.get_doc("Cost Center",d.name)
if not cost_center_doc.enable_distributed_cost_center:
DCC_allocation = frappe.db.sql("""SELECT parent, sum(percentage_allocation) as percentage_allocation
FROM `tabDistributed Cost Center`
WHERE cost_center IN %(cost_center)s
AND parent NOT IN %(cost_center)s
GROUP BY parent""",{'cost_center': [d.name]})
if DCC_allocation:
for account in new_accounts:
if account['name'] == DCC_allocation[0][0]:
for value in value_fields:
d[value] += account[value]*(DCC_allocation[0][1]/100)
for key in value_fields: for key in value_fields:
row[key] = flt(d.get(key, 0.0), 3) row[key] = flt(d.get(key, 0.0), 3)

View File

@@ -61,7 +61,7 @@ class TestTaxDetail(unittest.TestCase):
# Create GL Entries: # Create GL Entries:
db_doc.submit() db_doc.submit()
else: else:
db_doc.insert() db_doc.insert(ignore_if_duplicate=True)
except frappe.exceptions.DuplicateEntryError: except frappe.exceptions.DuplicateEntryError:
pass pass

View File

@@ -23,7 +23,7 @@ def validate_filters(filters):
def get_result(filters, tds_docs, tds_accounts, tax_category_map): def get_result(filters, tds_docs, tds_accounts, tax_category_map):
supplier_map = get_supplier_pan_map() supplier_map = get_supplier_pan_map()
tax_rate_map = get_tax_rate_map(filters) tax_rate_map = get_tax_rate_map(filters)
gle_map = get_gle_map(filters, tds_docs) gle_map = get_gle_map(tds_docs)
out = [] out = []
for name, details in gle_map.items(): for name, details in gle_map.items():
@@ -43,7 +43,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map):
if entry.account in tds_accounts: if entry.account in tds_accounts:
tds_deducted += (entry.credit - entry.debit) tds_deducted += (entry.credit - entry.debit)
total_amount_credited += (entry.credit - entry.debit) total_amount_credited += entry.credit
if tds_deducted: if tds_deducted:
row = { row = {
@@ -78,7 +78,7 @@ def get_supplier_pan_map():
return supplier_map return supplier_map
def get_gle_map(filters, documents): def get_gle_map(documents):
# create gle_map of the form # create gle_map of the form
# {"purchase_invoice": list of dict of all gle created for this invoice} # {"purchase_invoice": list of dict of all gle created for this invoice}
gle_map = {} gle_map = {}
@@ -86,7 +86,7 @@ def get_gle_map(filters, documents):
gle = frappe.db.get_all('GL Entry', gle = frappe.db.get_all('GL Entry',
{ {
"voucher_no": ["in", documents], "voucher_no": ["in", documents],
"credit": (">", 0) "is_cancelled": 0
}, },
["credit", "debit", "account", "voucher_no", "posting_date", "voucher_type", "against", "party"], ["credit", "debit", "account", "voucher_no", "posting_date", "voucher_type", "against", "party"],
) )
@@ -184,21 +184,28 @@ def get_tds_docs(filters):
payment_entries = [] payment_entries = []
journal_entries = [] journal_entries = []
tax_category_map = {} tax_category_map = {}
or_filters = {}
bank_accounts = frappe.get_all('Account', {'is_group': 0, 'account_type': 'Bank'}, pluck="name")
tds_accounts = frappe.get_all("Tax Withholding Account", {'company': filters.get('company')}, tds_accounts = frappe.get_all("Tax Withholding Account", {'company': filters.get('company')},
pluck="account") pluck="account")
query_filters = { query_filters = {
"credit": ('>', 0),
"account": ("in", tds_accounts), "account": ("in", tds_accounts),
"posting_date": ("between", [filters.get("from_date"), filters.get("to_date")]), "posting_date": ("between", [filters.get("from_date"), filters.get("to_date")]),
"is_cancelled": 0 "is_cancelled": 0,
"against": ("not in", bank_accounts)
} }
if filters.get('supplier'): if filters.get("supplier"):
query_filters.update({'against': filters.get('supplier')}) del query_filters["account"]
del query_filters["against"]
or_filters = {
"against": filters.get('supplier'),
"party": filters.get('supplier')
}
tds_docs = frappe.get_all("GL Entry", query_filters, ["voucher_no", "voucher_type", "against", "party"]) tds_docs = frappe.get_all("GL Entry", filters=query_filters, or_filters=or_filters, fields=["voucher_no", "voucher_type", "against", "party"])
for d in tds_docs: for d in tds_docs:
if d.voucher_type == "Purchase Invoice": if d.voucher_type == "Purchase Invoice":

View File

@@ -39,10 +39,11 @@ class TestReports(unittest.TestCase):
def test_execute_all_accounts_reports(self): def test_execute_all_accounts_reports(self):
"""Test that all script report in stock modules are executable with supported filters""" """Test that all script report in stock modules are executable with supported filters"""
for report, filter in REPORT_FILTER_TEST_CASES: for report, filter in REPORT_FILTER_TEST_CASES:
execute_script_report( with self.subTest(report=report):
report_name=report, execute_script_report(
module="Accounts", report_name=report,
filters=filter, module="Accounts",
default_filters=DEFAULT_FILTERS, filters=filter,
optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None, default_filters=DEFAULT_FILTERS,
) optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
)

View File

@@ -847,7 +847,7 @@ def create_payment_gateway_account(gateway, payment_channel="Email"):
"payment_account": bank_account.name, "payment_account": bank_account.name,
"currency": bank_account.account_currency, "currency": bank_account.account_currency,
"payment_channel": payment_channel "payment_channel": payment_channel
}).insert(ignore_permissions=True) }).insert(ignore_permissions=True, ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
# already exists, due to a reinstall? # already exists, due to a reinstall?

View File

@@ -1023,6 +1023,17 @@
"onboard": 0, "onboard": 0,
"type": "Link" "type": "Link"
}, },
{
"dependencies": "Cost Center",
"hidden": 0,
"is_query_report": 0,
"label": "Cost Center Allocation",
"link_count": 0,
"link_to": "Cost Center Allocation",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{ {
"dependencies": "Cost Center", "dependencies": "Cost Center",
"hidden": 0, "hidden": 0,

View File

@@ -108,6 +108,10 @@ frappe.ui.form.on('Asset', {
frm.trigger("create_asset_repair"); frm.trigger("create_asset_repair");
}, __("Manage")); }, __("Manage"));
frm.add_custom_button(__("Split Asset"), function() {
frm.trigger("split_asset");
}, __("Manage"));
if (frm.doc.status != 'Fully Depreciated') { if (frm.doc.status != 'Fully Depreciated') {
frm.add_custom_button(__("Adjust Asset Value"), function() { frm.add_custom_button(__("Adjust Asset Value"), function() {
frm.trigger("create_asset_value_adjustment"); frm.trigger("create_asset_value_adjustment");
@@ -322,6 +326,43 @@ frappe.ui.form.on('Asset', {
}); });
}, },
split_asset: function(frm) {
const title = __('Split Asset');
const fields = [
{
fieldname: 'split_qty',
fieldtype: 'Int',
label: __('Split Qty'),
reqd: 1
}
];
let dialog = new frappe.ui.Dialog({
title: title,
fields: fields
});
dialog.set_primary_action(__('Split'), function() {
const dialog_data = dialog.get_values();
frappe.call({
args: {
"asset_name": frm.doc.name,
"split_qty": cint(dialog_data.split_qty)
},
method: "erpnext.assets.doctype.asset.asset.split_asset",
callback: function(r) {
let doclist = frappe.model.sync(r.message);
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
}
});
dialog.hide();
});
dialog.show();
},
create_asset_value_adjustment: function(frm) { create_asset_value_adjustment: function(frm) {
frappe.call({ frappe.call({
args: { args: {

View File

@@ -3,7 +3,7 @@
"allow_import": 1, "allow_import": 1,
"allow_rename": 1, "allow_rename": 1,
"autoname": "naming_series:", "autoname": "naming_series:",
"creation": "2016-03-01 17:01:27.920130", "creation": "2022-01-18 02:26:55.975005",
"doctype": "DocType", "doctype": "DocType",
"document_type": "Document", "document_type": "Document",
"engine": "InnoDB", "engine": "InnoDB",
@@ -23,6 +23,7 @@
"asset_name", "asset_name",
"asset_category", "asset_category",
"location", "location",
"split_from",
"custodian", "custodian",
"department", "department",
"disposal_date", "disposal_date",
@@ -142,6 +143,7 @@
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
"fetch_from": "item_code.image",
"fieldname": "image", "fieldname": "image",
"fieldtype": "Attach Image", "fieldtype": "Attach Image",
"hidden": 1, "hidden": 1,
@@ -482,6 +484,13 @@
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Finance Books" "label": "Finance Books"
}, },
{
"fieldname": "split_from",
"fieldtype": "Link",
"label": "Split From",
"options": "Asset",
"read_only": 1
},
{ {
"fieldname": "asset_quantity", "fieldname": "asset_quantity",
"fieldtype": "Int", "fieldtype": "Int",
@@ -509,7 +518,7 @@
"link_fieldname": "asset" "link_fieldname": "asset"
} }
], ],
"modified": "2022-01-18 12:57:36.741192", "modified": "2022-01-30 20:19:24.680027",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset", "name": "Asset",

View File

@@ -38,7 +38,8 @@ class Asset(AccountsController):
self.validate_item() self.validate_item()
self.validate_cost_center() self.validate_cost_center()
self.set_missing_values() self.set_missing_values()
self.prepare_depreciation_data() if not self.split_from:
self.prepare_depreciation_data()
self.validate_gross_and_purchase_amount() self.validate_gross_and_purchase_amount()
if self.get("schedules"): if self.get("schedules"):
self.validate_expected_value_after_useful_life() self.validate_expected_value_after_useful_life()
@@ -202,143 +203,143 @@ class Asset(AccountsController):
start = self.clear_depreciation_schedule() start = self.clear_depreciation_schedule()
for finance_book in self.get('finance_books'): for finance_book in self.get('finance_books'):
self.validate_asset_finance_books(finance_book) self._make_depreciation_schedule(finance_book, start, date_of_sale)
# value_after_depreciation - current Asset value def _make_depreciation_schedule(self, finance_book, start, date_of_sale):
if self.docstatus == 1 and finance_book.value_after_depreciation: self.validate_asset_finance_books(finance_book)
value_after_depreciation = flt(finance_book.value_after_depreciation)
else:
value_after_depreciation = (flt(self.gross_purchase_amount) -
flt(self.opening_accumulated_depreciation))
finance_book.value_after_depreciation = value_after_depreciation value_after_depreciation = self._get_value_after_depreciation(finance_book)
finance_book.value_after_depreciation = value_after_depreciation
number_of_pending_depreciations = cint(finance_book.total_number_of_depreciations) - \ number_of_pending_depreciations = cint(finance_book.total_number_of_depreciations) - \
cint(self.number_of_depreciations_booked) cint(self.number_of_depreciations_booked)
has_pro_rata = self.check_is_pro_rata(finance_book) has_pro_rata = self.check_is_pro_rata(finance_book)
if has_pro_rata:
number_of_pending_depreciations += 1
if has_pro_rata: skip_row = False
number_of_pending_depreciations += 1
skip_row = False for n in range(start[finance_book.idx-1], number_of_pending_depreciations):
# If depreciation is already completed (for double declining balance)
if skip_row: continue
for n in range(start[finance_book.idx-1], number_of_pending_depreciations): depreciation_amount = get_depreciation_amount(self, value_after_depreciation, finance_book)
# If depreciation is already completed (for double declining balance)
if skip_row: continue
depreciation_amount = get_depreciation_amount(self, value_after_depreciation, finance_book) if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1:
schedule_date = add_months(finance_book.depreciation_start_date,
n * cint(finance_book.frequency_of_depreciation))
if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1: # schedule date will be a year later from start date
schedule_date = add_months(finance_book.depreciation_start_date, # so monthly schedule date is calculated by removing 11 months from it
n * cint(finance_book.frequency_of_depreciation)) monthly_schedule_date = add_months(schedule_date, - finance_book.frequency_of_depreciation + 1)
# schedule date will be a year later from start date # if asset is being sold
# so monthly schedule date is calculated by removing 11 months from it if date_of_sale:
monthly_schedule_date = add_months(schedule_date, - finance_book.frequency_of_depreciation + 1) from_date = self.get_from_date(finance_book.finance_book)
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
# if asset is being sold from_date, date_of_sale)
if date_of_sale:
from_date = self.get_from_date(finance_book.finance_book)
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
from_date, date_of_sale)
if depreciation_amount > 0:
self.append("schedules", {
"schedule_date": date_of_sale,
"depreciation_amount": depreciation_amount,
"depreciation_method": finance_book.depreciation_method,
"finance_book": finance_book.finance_book,
"finance_book_id": finance_book.idx
})
break
# For first row
if has_pro_rata and not self.opening_accumulated_depreciation and n==0:
from_date = add_days(self.available_for_use_date, -1) # needed to calc depr amount for available_for_use_date too
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
from_date, finance_book.depreciation_start_date)
# For first depr schedule date will be the start date
# so monthly schedule date is calculated by removing month difference between use date and start date
monthly_schedule_date = add_months(finance_book.depreciation_start_date, - months + 1)
# For last row
elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
if not self.flags.increase_in_asset_life:
# In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission
self.to_date = add_months(self.available_for_use_date,
(n + self.number_of_depreciations_booked) * cint(finance_book.frequency_of_depreciation))
depreciation_amount_without_pro_rata = depreciation_amount
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book,
depreciation_amount, schedule_date, self.to_date)
depreciation_amount = self.get_adjusted_depreciation_amount(depreciation_amount_without_pro_rata,
depreciation_amount, finance_book.finance_book)
monthly_schedule_date = add_months(schedule_date, 1)
schedule_date = add_days(schedule_date, days)
last_schedule_date = schedule_date
if not depreciation_amount: continue
value_after_depreciation -= flt(depreciation_amount,
self.precision("gross_purchase_amount"))
# Adjust depreciation amount in the last period based on the expected value after useful life
if finance_book.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1
and value_after_depreciation != finance_book.expected_value_after_useful_life)
or value_after_depreciation < finance_book.expected_value_after_useful_life):
depreciation_amount += (value_after_depreciation - finance_book.expected_value_after_useful_life)
skip_row = True
if depreciation_amount > 0: if depreciation_amount > 0:
# With monthly depreciation, each depreciation is divided by months remaining until next date self._add_depreciation_row(date_of_sale, depreciation_amount, finance_book.depreciation_method,
if self.allow_monthly_depreciation: finance_book.finance_book, finance_book.idx)
# month range is 1 to 12
# In pro rata case, for first and last depreciation, month range would be different
month_range = months \
if (has_pro_rata and n==0) or (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) \
else finance_book.frequency_of_depreciation
for r in range(month_range): break
if (has_pro_rata and n == 0):
# For first entry of monthly depr # For first row
if r == 0: if has_pro_rata and not self.opening_accumulated_depreciation and n==0:
days_until_first_depr = date_diff(monthly_schedule_date, self.available_for_use_date) from_date = add_days(self.available_for_use_date, -1) # needed to calc depr amount for available_for_use_date too
per_day_amt = depreciation_amount / days depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
depreciation_amount_for_current_month = per_day_amt * days_until_first_depr from_date, finance_book.depreciation_start_date)
depreciation_amount -= depreciation_amount_for_current_month
date = monthly_schedule_date # For first depr schedule date will be the start date
amount = depreciation_amount_for_current_month # so monthly schedule date is calculated by removing month difference between use date and start date
else: monthly_schedule_date = add_months(finance_book.depreciation_start_date, - months + 1)
date = add_months(monthly_schedule_date, r)
amount = depreciation_amount / (month_range - 1) # For last row
elif (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) and r == cint(month_range) - 1: elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
# For last entry of monthly depr if not self.flags.increase_in_asset_life:
date = last_schedule_date # In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission
amount = depreciation_amount / month_range self.to_date = add_months(self.available_for_use_date,
(n + self.number_of_depreciations_booked) * cint(finance_book.frequency_of_depreciation))
depreciation_amount_without_pro_rata = depreciation_amount
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book,
depreciation_amount, schedule_date, self.to_date)
depreciation_amount = self.get_adjusted_depreciation_amount(depreciation_amount_without_pro_rata,
depreciation_amount, finance_book.finance_book)
monthly_schedule_date = add_months(schedule_date, 1)
schedule_date = add_days(schedule_date, days)
last_schedule_date = schedule_date
if not depreciation_amount: continue
value_after_depreciation -= flt(depreciation_amount,
self.precision("gross_purchase_amount"))
# Adjust depreciation amount in the last period based on the expected value after useful life
if finance_book.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1
and value_after_depreciation != finance_book.expected_value_after_useful_life)
or value_after_depreciation < finance_book.expected_value_after_useful_life):
depreciation_amount += (value_after_depreciation - finance_book.expected_value_after_useful_life)
skip_row = True
if depreciation_amount > 0:
# With monthly depreciation, each depreciation is divided by months remaining until next date
if self.allow_monthly_depreciation:
# month range is 1 to 12
# In pro rata case, for first and last depreciation, month range would be different
month_range = months \
if (has_pro_rata and n==0) or (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) \
else finance_book.frequency_of_depreciation
for r in range(month_range):
if (has_pro_rata and n == 0):
# For first entry of monthly depr
if r == 0:
days_until_first_depr = date_diff(monthly_schedule_date, self.available_for_use_date)
per_day_amt = depreciation_amount / days
depreciation_amount_for_current_month = per_day_amt * days_until_first_depr
depreciation_amount -= depreciation_amount_for_current_month
date = monthly_schedule_date
amount = depreciation_amount_for_current_month
else: else:
date = add_months(monthly_schedule_date, r) date = add_months(monthly_schedule_date, r)
amount = depreciation_amount / month_range amount = depreciation_amount / (month_range - 1)
elif (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) and r == cint(month_range) - 1:
# For last entry of monthly depr
date = last_schedule_date
amount = depreciation_amount / month_range
else:
date = add_months(monthly_schedule_date, r)
amount = depreciation_amount / month_range
self.append("schedules", { self._add_depreciation_row(date, amount, finance_book.depreciation_method,
"schedule_date": date, finance_book.finance_book, finance_book.idx)
"depreciation_amount": amount, else:
"depreciation_method": finance_book.depreciation_method, self._add_depreciation_row(schedule_date, depreciation_amount, finance_book.depreciation_method,
"finance_book": finance_book.finance_book, finance_book.finance_book, finance_book.idx)
"finance_book_id": finance_book.idx
}) def _add_depreciation_row(self, schedule_date, depreciation_amount, depreciation_method, finance_book, finance_book_id):
else: self.append("schedules", {
self.append("schedules", { "schedule_date": schedule_date,
"schedule_date": schedule_date, "depreciation_amount": depreciation_amount,
"depreciation_amount": depreciation_amount, "depreciation_method": depreciation_method,
"depreciation_method": finance_book.depreciation_method, "finance_book": finance_book,
"finance_book": finance_book.finance_book, "finance_book_id": finance_book_id
"finance_book_id": finance_book.idx })
})
def _get_value_after_depreciation(self, finance_book):
# value_after_depreciation - current Asset value
if self.docstatus == 1 and finance_book.value_after_depreciation:
value_after_depreciation = flt(finance_book.value_after_depreciation)
else:
value_after_depreciation = (flt(self.gross_purchase_amount) -
flt(self.opening_accumulated_depreciation))
return value_after_depreciation
# depreciation schedules need to be cleared before modification due to increase in asset life/asset sales # depreciation schedules need to be cleared before modification due to increase in asset life/asset sales
# JE: Journal Entry, FB: Finance Book # JE: Journal Entry, FB: Finance Book
@@ -348,7 +349,6 @@ class Asset(AccountsController):
depr_schedule = [] depr_schedule = []
for schedule in self.get('schedules'): for schedule in self.get('schedules'):
# to update start when there are JEs linked with all the schedule rows corresponding to an FB # to update start when there are JEs linked with all the schedule rows corresponding to an FB
if len(start) == (int(schedule.finance_book_id) - 2): if len(start) == (int(schedule.finance_book_id) - 2):
start.append(num_of_depreciations_completed) start.append(num_of_depreciations_completed)
@@ -417,11 +417,12 @@ class Asset(AccountsController):
def validate_asset_finance_books(self, row): def validate_asset_finance_books(self, row):
if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount): if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount):
frappe.throw(_("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount") frappe.throw(_("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount")
.format(row.idx)) .format(row.idx), title=_("Invalid Schedule"))
if not row.depreciation_start_date: if not row.depreciation_start_date:
if not self.available_for_use_date: if not self.available_for_use_date:
frappe.throw(_("Row {0}: Depreciation Start Date is required").format(row.idx)) frappe.throw(_("Row {0}: Depreciation Start Date is required")
.format(row.idx), title=_("Invalid Schedule"))
row.depreciation_start_date = get_last_day(self.available_for_use_date) row.depreciation_start_date = get_last_day(self.available_for_use_date)
if not self.is_existing_asset: if not self.is_existing_asset:
@@ -439,8 +440,9 @@ class Asset(AccountsController):
else: else:
self.number_of_depreciations_booked = 0 self.number_of_depreciations_booked = 0
if cint(self.number_of_depreciations_booked) > cint(row.total_number_of_depreciations): if flt(row.total_number_of_depreciations) <= cint(self.number_of_depreciations_booked):
frappe.throw(_("Number of Depreciations Booked cannot be greater than Total Number of Depreciations")) frappe.throw(_("Row {0}: Total Number of Depreciations cannot be less than or equal to Number of Depreciations Booked")
.format(row.idx), title=_("Invalid Schedule"))
if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate(self.purchase_date): if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate(self.purchase_date):
frappe.throw(_("Depreciation Row {0}: Next Depreciation Date cannot be before Purchase Date") frappe.throw(_("Depreciation Row {0}: Next Depreciation Date cannot be before Purchase Date")
@@ -924,3 +926,113 @@ def get_depreciation_amount(asset, depreciable_value, row):
depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100)) depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100))
return depreciation_amount return depreciation_amount
@frappe.whitelist()
def split_asset(asset_name, split_qty):
asset = frappe.get_doc("Asset", asset_name)
split_qty = cint(split_qty)
if split_qty >= asset.asset_quantity:
frappe.throw(_("Split qty cannot be grater than or equal to asset qty"))
remaining_qty = asset.asset_quantity - split_qty
new_asset = create_new_asset_after_split(asset, split_qty)
update_existing_asset(asset, remaining_qty)
return new_asset
def update_existing_asset(asset, remaining_qty):
remaining_gross_purchase_amount = flt((asset.gross_purchase_amount * remaining_qty) / asset.asset_quantity)
opening_accumulated_depreciation = flt((asset.opening_accumulated_depreciation * remaining_qty) / asset.asset_quantity)
frappe.db.set_value("Asset", asset.name, {
'opening_accumulated_depreciation': opening_accumulated_depreciation,
'gross_purchase_amount': remaining_gross_purchase_amount,
'asset_quantity': remaining_qty
})
for finance_book in asset.get('finance_books'):
value_after_depreciation = flt((finance_book.value_after_depreciation * remaining_qty)/asset.asset_quantity)
expected_value_after_useful_life = flt((finance_book.expected_value_after_useful_life * remaining_qty)/asset.asset_quantity)
frappe.db.set_value('Asset Finance Book', finance_book.name, 'value_after_depreciation', value_after_depreciation)
frappe.db.set_value('Asset Finance Book', finance_book.name, 'expected_value_after_useful_life', expected_value_after_useful_life)
accumulated_depreciation = 0
for term in asset.get('schedules'):
depreciation_amount = flt((term.depreciation_amount * remaining_qty)/asset.asset_quantity)
frappe.db.set_value('Depreciation Schedule', term.name, 'depreciation_amount', depreciation_amount)
accumulated_depreciation += depreciation_amount
frappe.db.set_value('Depreciation Schedule', term.name, 'accumulated_depreciation_amount', accumulated_depreciation)
def create_new_asset_after_split(asset, split_qty):
new_asset = frappe.copy_doc(asset)
new_gross_purchase_amount = flt((asset.gross_purchase_amount * split_qty) / asset.asset_quantity)
opening_accumulated_depreciation = flt((asset.opening_accumulated_depreciation * split_qty) / asset.asset_quantity)
new_asset.gross_purchase_amount = new_gross_purchase_amount
new_asset.opening_accumulated_depreciation = opening_accumulated_depreciation
new_asset.asset_quantity = split_qty
new_asset.split_from = asset.name
accumulated_depreciation = 0
for finance_book in new_asset.get('finance_books'):
finance_book.value_after_depreciation = flt((finance_book.value_after_depreciation * split_qty)/asset.asset_quantity)
finance_book.expected_value_after_useful_life = flt((finance_book.expected_value_after_useful_life * split_qty)/asset.asset_quantity)
for term in new_asset.get('schedules'):
depreciation_amount = flt((term.depreciation_amount * split_qty)/asset.asset_quantity)
term.depreciation_amount = depreciation_amount
accumulated_depreciation += depreciation_amount
term.accumulated_depreciation_amount = accumulated_depreciation
new_asset.submit()
new_asset.set_status()
for term in new_asset.get('schedules'):
# Update references in JV
if term.journal_entry:
add_reference_in_jv_on_split(term.journal_entry, new_asset.name, asset.name, term.depreciation_amount)
return new_asset
def add_reference_in_jv_on_split(entry_name, new_asset_name, old_asset_name, depreciation_amount):
journal_entry = frappe.get_doc('Journal Entry', entry_name)
entries_to_add = []
idx = len(journal_entry.get('accounts')) + 1
for account in journal_entry.get('accounts'):
if account.reference_name == old_asset_name:
entries_to_add.append(frappe.copy_doc(account).as_dict())
if account.credit:
account.credit = account.credit - depreciation_amount
account.credit_in_account_currency = account.credit_in_account_currency - \
account.exchange_rate * depreciation_amount
elif account.debit:
account.debit = account.debit - depreciation_amount
account.debit_in_account_currency = account.debit_in_account_currency - \
account.exchange_rate * depreciation_amount
for entry in entries_to_add:
entry.reference_name = new_asset_name
if entry.credit:
entry.credit = depreciation_amount
entry.credit_in_account_currency = entry.exchange_rate * depreciation_amount
elif entry.debit:
entry.debit = depreciation_amount
entry.debit_in_account_currency = entry.exchange_rate * depreciation_amount
entry.idx = idx
idx += 1
journal_entry.append('accounts', entry)
journal_entry.flags.ignore_validate_update_after_submit = True
journal_entry.save()
# Repost GL Entries
journal_entry.docstatus = 2
journal_entry.make_gl_entries(1)
journal_entry.docstatus = 1
journal_entry.make_gl_entries()

View File

@@ -7,7 +7,7 @@ import frappe
from frappe.utils import add_days, add_months, cstr, flt, get_last_day, getdate, nowdate from frappe.utils import add_days, add_months, cstr, flt, get_last_day, getdate, nowdate
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 make_sales_invoice from erpnext.assets.doctype.asset.asset import make_sales_invoice, split_asset
from erpnext.assets.doctype.asset.depreciation import ( from erpnext.assets.doctype.asset.depreciation import (
post_depreciation_entries, post_depreciation_entries,
restore_asset, restore_asset,
@@ -245,6 +245,57 @@ class TestAsset(AssetSetup):
si.cancel() si.cancel()
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated") self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated")
def test_asset_splitting(self):
asset = create_asset(
calculate_depreciation = 1,
asset_quantity=10,
available_for_use_date = '2020-01-01',
purchase_date = '2020-01-01',
expected_value_after_useful_life = 0,
total_number_of_depreciations = 6,
number_of_depreciations_booked = 1,
frequency_of_depreciation = 10,
depreciation_start_date = '2021-01-01',
opening_accumulated_depreciation=20000,
gross_purchase_amount=120000,
submit = 1
)
post_depreciation_entries(date="2021-01-01")
self.assertEqual(asset.asset_quantity, 10)
self.assertEqual(asset.gross_purchase_amount, 120000)
self.assertEqual(asset.opening_accumulated_depreciation, 20000)
new_asset = split_asset(asset.name, 2)
asset.load_from_db()
self.assertEqual(new_asset.asset_quantity, 2)
self.assertEqual(new_asset.gross_purchase_amount, 24000)
self.assertEqual(new_asset.opening_accumulated_depreciation, 4000)
self.assertEqual(new_asset.split_from, asset.name)
self.assertEqual(new_asset.schedules[0].depreciation_amount, 4000)
self.assertEqual(new_asset.schedules[1].depreciation_amount, 4000)
self.assertEqual(asset.asset_quantity, 8)
self.assertEqual(asset.gross_purchase_amount, 96000)
self.assertEqual(asset.opening_accumulated_depreciation, 16000)
self.assertEqual(asset.schedules[0].depreciation_amount, 16000)
self.assertEqual(asset.schedules[1].depreciation_amount, 16000)
journal_entry = asset.schedules[0].journal_entry
jv = frappe.get_doc('Journal Entry', journal_entry)
self.assertEqual(jv.accounts[0].credit_in_account_currency, 16000)
self.assertEqual(jv.accounts[1].debit_in_account_currency, 16000)
self.assertEqual(jv.accounts[2].credit_in_account_currency, 4000)
self.assertEqual(jv.accounts[3].debit_in_account_currency, 4000)
self.assertEqual(jv.accounts[0].reference_name, asset.name)
self.assertEqual(jv.accounts[1].reference_name, asset.name)
self.assertEqual(jv.accounts[2].reference_name, new_asset.name)
self.assertEqual(jv.accounts[3].reference_name, new_asset.name)
def test_expense_head(self): def test_expense_head(self):
pr = make_purchase_receipt(item_code="Macbook Pro", pr = make_purchase_receipt(item_code="Macbook Pro",
qty=2, rate=200000.0, location="Test Location") qty=2, rate=200000.0, location="Test Location")
@@ -822,8 +873,9 @@ class TestDepreciationBasics(AssetSetup):
self.assertRaises(frappe.ValidationError, asset.save) self.assertRaises(frappe.ValidationError, asset.save)
def test_number_of_depreciations(self): def test_number_of_depreciations(self):
"""Tests if an error is raised when number_of_depreciations_booked > total_number_of_depreciations.""" """Tests if an error is raised when number_of_depreciations_booked >= total_number_of_depreciations."""
# number_of_depreciations_booked > total_number_of_depreciations
asset = create_asset( asset = create_asset(
item_code = "Macbook Pro", item_code = "Macbook Pro",
calculate_depreciation = 1, calculate_depreciation = 1,
@@ -838,6 +890,21 @@ class TestDepreciationBasics(AssetSetup):
self.assertRaises(frappe.ValidationError, asset.save) self.assertRaises(frappe.ValidationError, asset.save)
# number_of_depreciations_booked = total_number_of_depreciations
asset_2 = create_asset(
item_code = "Macbook Pro",
calculate_depreciation = 1,
available_for_use_date = "2019-12-31",
total_number_of_depreciations = 5,
expected_value_after_useful_life = 10000,
depreciation_start_date = "2020-07-01",
opening_accumulated_depreciation = 10000,
number_of_depreciations_booked = 5,
do_not_save = 1
)
self.assertRaises(frappe.ValidationError, asset_2.save)
def test_depreciation_start_date_is_before_purchase_date(self): def test_depreciation_start_date_is_before_purchase_date(self):
asset = create_asset( asset = create_asset(
item_code = "Macbook Pro", item_code = "Macbook Pro",
@@ -1197,7 +1264,8 @@ def create_asset(**args):
"available_for_use_date": args.available_for_use_date or "2020-06-06", "available_for_use_date": args.available_for_use_date or "2020-06-06",
"location": args.location or "Test Location", "location": args.location or "Test Location",
"asset_owner": args.asset_owner or "Company", "asset_owner": args.asset_owner or "Company",
"is_existing_asset": args.is_existing_asset or 1 "is_existing_asset": args.is_existing_asset or 1,
"asset_quantity": args.get("asset_quantity") or 1
}) })
if asset.calculate_depreciation: if asset.calculate_depreciation:
@@ -1212,7 +1280,7 @@ def create_asset(**args):
if not args.do_not_save: if not args.do_not_save:
try: try:
asset.save() asset.insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
@@ -1253,7 +1321,7 @@ def create_fixed_asset_item(item_code=None, auto_create_assets=1, is_grouped_ass
"is_grouped_asset": is_grouped_asset, "is_grouped_asset": is_grouped_asset,
"asset_naming_series": naming_series "asset_naming_series": naming_series
}) })
item.insert() item.insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass
return item return item

View File

@@ -23,7 +23,7 @@ class TestAssetCategory(unittest.TestCase):
}) })
try: try:
asset_category.insert() asset_category.insert(ignore_if_duplicate=True)
except frappe.DuplicateEntryError: except frappe.DuplicateEntryError:
pass pass

View File

@@ -0,0 +1,34 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Bulk Transaction Log', {
before_load: function(frm) {
query(frm);
},
refresh: function(frm) {
frm.disable_save();
frm.add_custom_button(__('Retry Failed Transactions'), ()=>{
frappe.confirm(__("Retry Failing Transactions ?"), ()=>{
query(frm);
}
);
});
}
});
function query(frm) {
frappe.call({
method: "erpnext.bulk_transaction.doctype.bulk_transaction_log.bulk_transaction_log.retry_failing_transaction",
args: {
log_date: frm.doc.log_date
}
}).then((r) => {
if (r.message) {
frm.remove_custom_button("Retry Failed Transactions");
} else {
frappe.show_alert(__("Retrying Failed Transactions"), 5);
}
});
}

View File

@@ -0,0 +1,51 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2021-11-30 13:41:16.343827",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"log_date",
"logger_data"
],
"fields": [
{
"fieldname": "log_date",
"fieldtype": "Date",
"label": "Log Date",
"read_only": 1
},
{
"fieldname": "logger_data",
"fieldtype": "Table",
"label": "Logger Data",
"options": "Bulk Transaction Log Detail"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-02-03 17:23:02.935325",
"modified_by": "Administrator",
"module": "Bulk Transaction",
"name": "Bulk Transaction Log",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -0,0 +1,66 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from datetime import date
import frappe
from frappe.model.document import Document
from erpnext.utilities.bulk_transaction import task, update_logger
class BulkTransactionLog(Document):
pass
@frappe.whitelist()
def retry_failing_transaction(log_date=None):
btp = frappe.qb.DocType("Bulk Transaction Log Detail")
data = (
frappe.qb.from_(btp)
.select(btp.transaction_name, btp.from_doctype, btp.to_doctype)
.distinct()
.where(btp.retried != 1)
.where(btp.transaction_status == "Failed")
.where(btp.date == log_date)
).run(as_dict=True)
if data:
if not log_date:
log_date = str(date.today())
if len(data) > 10:
frappe.enqueue(job, queue="long", job_name="bulk_retry", data=data, log_date=log_date)
else:
job(data, log_date)
else:
return "No Failed Records"
def job(data, log_date):
for d in data:
failed = []
try:
frappe.db.savepoint("before_creation_of_record")
task(d.transaction_name, d.from_doctype, d.to_doctype)
except Exception as e:
frappe.db.rollback(save_point="before_creation_of_record")
failed.append(e)
update_logger(
d.transaction_name,
e,
d.from_doctype,
d.to_doctype,
status="Failed",
log_date=log_date,
restarted=1
)
if not failed:
update_logger(
d.transaction_name,
None,
d.from_doctype,
d.to_doctype,
status="Success",
log_date=log_date,
restarted=1,
)

View File

@@ -0,0 +1,81 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
from datetime import date
import frappe
from erpnext.utilities.bulk_transaction import transaction_processing
class TestBulkTransactionLog(unittest.TestCase):
def setUp(self):
create_company()
create_customer()
create_item()
def test_for_single_record(self):
so_name = create_so()
transaction_processing([{"name": so_name}], "Sales Order", "Sales Invoice")
data = frappe.db.get_list("Sales Invoice", filters = {"posting_date": date.today(), "customer": "Bulk Customer"}, fields=["*"])
if not data:
self.fail("No Sales Invoice Created !")
def test_entry_in_log(self):
so_name = create_so()
transaction_processing([{"name": so_name}], "Sales Order", "Sales Invoice")
doc = frappe.get_doc("Bulk Transaction Log", str(date.today()))
for d in doc.get("logger_data"):
if d.transaction_name == so_name:
self.assertEqual(d.transaction_name, so_name)
self.assertEqual(d.transaction_status, "Success")
self.assertEqual(d.from_doctype, "Sales Order")
self.assertEqual(d.to_doctype, "Sales Invoice")
self.assertEqual(d.retried, 0)
def create_company():
if not frappe.db.exists('Company', '_Test Company'):
frappe.get_doc({
'doctype': 'Company',
'company_name': '_Test Company',
'country': 'India',
'default_currency': 'INR'
}).insert()
def create_customer():
if not frappe.db.exists('Customer', 'Bulk Customer'):
frappe.get_doc({
'doctype': 'Customer',
'customer_name': 'Bulk Customer'
}).insert()
def create_item():
if not frappe.db.exists("Item", "MK"):
frappe.get_doc({
"doctype": "Item",
"item_code": "MK",
"item_name": "Milk",
"description": "Milk",
"item_group": "Products"
}).insert()
def create_so(intent=None):
so = frappe.new_doc("Sales Order")
so.customer = "Bulk Customer"
so.company = "_Test Company"
so.transaction_date = date.today()
so.set_warehouse = "Finished Goods - _TC"
so.append("items", {
"item_code": "MK",
"delivery_date": date.today(),
"qty": 10,
"rate": 80,
})
so.insert()
so.submit()
return so.name

View File

@@ -0,0 +1,86 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2021-11-30 13:38:30.926047",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"transaction_name",
"date",
"time",
"transaction_status",
"error_description",
"from_doctype",
"to_doctype",
"retried"
],
"fields": [
{
"fieldname": "transaction_name",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Name",
"options": "from_doctype"
},
{
"fieldname": "transaction_status",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Status",
"read_only": 1
},
{
"fieldname": "error_description",
"fieldtype": "Long Text",
"label": "Error Description",
"read_only": 1
},
{
"fieldname": "from_doctype",
"fieldtype": "Link",
"label": "From Doctype",
"options": "DocType",
"read_only": 1
},
{
"fieldname": "to_doctype",
"fieldtype": "Link",
"label": "To Doctype",
"options": "DocType",
"read_only": 1
},
{
"fieldname": "date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Date ",
"read_only": 1
},
{
"fieldname": "time",
"fieldtype": "Time",
"label": "Time",
"read_only": 1
},
{
"fieldname": "retried",
"fieldtype": "Int",
"label": "Retried",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2022-02-03 19:57:31.650359",
"modified_by": "Administrator",
"module": "Bulk Transaction",
"name": "Bulk Transaction Log Detail",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -0,0 +1,9 @@
# 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 BulkTransactionLogDetail(Document):
pass

View File

@@ -6,14 +6,17 @@
"document_type": "Other", "document_type": "Other",
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"supplier_and_price_defaults_section",
"supp_master_name", "supp_master_name",
"supplier_group", "supplier_group",
"column_break_4",
"buying_price_list", "buying_price_list",
"maintain_same_rate_action", "maintain_same_rate_action",
"role_to_override_stop_action", "role_to_override_stop_action",
"column_break_3", "transaction_settings_section",
"po_required", "po_required",
"pr_required", "pr_required",
"column_break_12",
"maintain_same_rate", "maintain_same_rate",
"allow_multiple_items", "allow_multiple_items",
"bill_for_rejected_quantity_in_purchase_invoice", "bill_for_rejected_quantity_in_purchase_invoice",
@@ -42,10 +45,6 @@
"label": "Default Buying Price List", "label": "Default Buying Price List",
"options": "Price List" "options": "Price List"
}, },
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{ {
"fieldname": "po_required", "fieldname": "po_required",
"fieldtype": "Select", "fieldtype": "Select",
@@ -73,7 +72,7 @@
{ {
"fieldname": "subcontract", "fieldname": "subcontract",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Subcontract" "label": "Subcontracting Settings"
}, },
{ {
"default": "Material Transferred for Subcontract", "default": "Material Transferred for Subcontract",
@@ -116,6 +115,24 @@
"fieldname": "bill_for_rejected_quantity_in_purchase_invoice", "fieldname": "bill_for_rejected_quantity_in_purchase_invoice",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Bill for Rejected Quantity in Purchase Invoice" "label": "Bill for Rejected Quantity in Purchase Invoice"
},
{
"fieldname": "supplier_and_price_defaults_section",
"fieldtype": "Section Break",
"label": "Supplier and Price Defaults"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "transaction_settings_section",
"fieldtype": "Section Break",
"label": "Transaction Settings"
},
{
"fieldname": "column_break_12",
"fieldtype": "Column Break"
} }
], ],
"icon": "fa fa-cog", "icon": "fa fa-cog",
@@ -123,7 +140,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2021-09-08 19:26:23.548837", "modified": "2022-01-27 17:57:58.367048",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Buying Settings", "name": "Buying Settings",
@@ -141,5 +158,6 @@
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -29,8 +29,22 @@ frappe.listview_settings['Purchase Order'] = {
listview.call_for_selected_items(method, { "status": "Closed" }); listview.call_for_selected_items(method, { "status": "Closed" });
}); });
listview.page.add_menu_item(__("Re-open"), function () { listview.page.add_menu_item(__("Reopen"), function () {
listview.call_for_selected_items(method, { "status": "Submitted" }); listview.call_for_selected_items(method, { "status": "Submitted" });
}); });
listview.page.add_action_item(__("Purchase Invoice"), ()=>{
erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Purchase Invoice");
});
listview.page.add_action_item(__("Purchase Receipt"), ()=>{
erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Purchase Receipt");
});
listview.page.add_action_item(__("Advance Payment"), ()=>{
erpnext.bulk_transaction_processing.create(listview, "Purchase Order", "Advance Payment");
});
} }
}; };

View File

@@ -682,17 +682,18 @@ class TestPurchaseOrder(unittest.TestCase):
bin1 = frappe.db.get_value("Bin", bin1 = frappe.db.get_value("Bin",
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
fieldname=["reserved_qty_for_sub_contract", "projected_qty"], as_dict=1) fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"], as_dict=1)
# Submit PO # Submit PO
po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes") po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes")
bin2 = frappe.db.get_value("Bin", bin2 = frappe.db.get_value("Bin",
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"}, filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
fieldname=["reserved_qty_for_sub_contract", "projected_qty"], as_dict=1) fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"], as_dict=1)
self.assertEqual(bin2.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10) self.assertEqual(bin2.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10)
self.assertEqual(bin2.projected_qty, bin1.projected_qty - 10) self.assertEqual(bin2.projected_qty, bin1.projected_qty - 10)
self.assertNotEqual(bin1.modified, bin2.modified)
# Create stock transfer # Create stock transfer
rm_item = [{"item_code":"_Test FG Item","rm_item_code":"_Test Item","item_name":"_Test Item", rm_item = [{"item_code":"_Test FG Item","rm_item_code":"_Test Item","item_name":"_Test Item",

View File

@@ -131,28 +131,6 @@ class Supplier(TransactionBase):
if frappe.defaults.get_global_default('supp_master_name') == 'Supplier Name': if frappe.defaults.get_global_default('supp_master_name') == 'Supplier Name':
frappe.db.set(self, "supplier_name", newdn) frappe.db.set(self, "supplier_name", newdn)
def create_onboarding_docs(self, args):
company = frappe.defaults.get_defaults().get('company') or \
frappe.db.get_single_value('Global Defaults', 'default_company')
for i in range(1, args.get('max_count')):
supplier = args.get('supplier_name_' + str(i))
if supplier:
try:
doc = frappe.get_doc({
'doctype': self.doctype,
'supplier_name': supplier,
'supplier_group': _('Local'),
'company': company
}).insert()
if args.get('supplier_email_' + str(i)):
from erpnext.selling.doctype.customer.customer import create_contact
create_contact(supplier, 'Supplier',
doc.name, args.get('supplier_email_' + str(i)))
except frappe.NameError:
pass
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_supplier_primary_contact(doctype, txt, searchfield, start, page_len, filters): def get_supplier_primary_contact(doctype, txt, searchfield, start, page_len, filters):

View File

@@ -14,151 +14,150 @@ test_records = frappe.get_test_records('Supplier')
class TestSupplier(unittest.TestCase): class TestSupplier(unittest.TestCase):
def test_get_supplier_group_details(self): def test_get_supplier_group_details(self):
doc = frappe.new_doc("Supplier Group") doc = frappe.new_doc("Supplier Group")
doc.supplier_group_name = "_Testing Supplier Group" doc.supplier_group_name = "_Testing Supplier Group"
doc.payment_terms = "_Test Payment Term Template 3" doc.payment_terms = "_Test Payment Term Template 3"
doc.accounts = [] doc.accounts = []
test_account_details = { test_account_details = {
"company": "_Test Company", "company": "_Test Company",
"account": "Creditors - _TC", "account": "Creditors - _TC",
} }
doc.append("accounts", test_account_details) doc.append("accounts", test_account_details)
doc.save() doc.save()
s_doc = frappe.new_doc("Supplier") s_doc = frappe.new_doc("Supplier")
s_doc.supplier_name = "Testing Supplier" s_doc.supplier_name = "Testing Supplier"
s_doc.supplier_group = "_Testing Supplier Group" s_doc.supplier_group = "_Testing Supplier Group"
s_doc.payment_terms = "" s_doc.payment_terms = ""
s_doc.accounts = [] s_doc.accounts = []
s_doc.insert() s_doc.insert()
s_doc.get_supplier_group_details() s_doc.get_supplier_group_details()
self.assertEqual(s_doc.payment_terms, "_Test Payment Term Template 3") self.assertEqual(s_doc.payment_terms, "_Test Payment Term Template 3")
self.assertEqual(s_doc.accounts[0].company, "_Test Company") self.assertEqual(s_doc.accounts[0].company, "_Test Company")
self.assertEqual(s_doc.accounts[0].account, "Creditors - _TC") self.assertEqual(s_doc.accounts[0].account, "Creditors - _TC")
s_doc.delete() s_doc.delete()
doc.delete() doc.delete()
def test_supplier_default_payment_terms(self): def test_supplier_default_payment_terms(self):
# Payment Term based on Days after invoice date # Payment Term based on Days after invoice date
frappe.db.set_value( frappe.db.set_value(
"Supplier", "_Test Supplier With Template 1", "payment_terms", "_Test Payment Term Template 3") "Supplier", "_Test Supplier With Template 1", "payment_terms", "_Test Payment Term Template 3")
due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1") due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1")
self.assertEqual(due_date, "2016-02-21") self.assertEqual(due_date, "2016-02-21")
due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1") due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1")
self.assertEqual(due_date, "2017-02-21") self.assertEqual(due_date, "2017-02-21")
# Payment Term based on last day of month # Payment Term based on last day of month
frappe.db.set_value( frappe.db.set_value(
"Supplier", "_Test Supplier With Template 1", "payment_terms", "_Test Payment Term Template 1") "Supplier", "_Test Supplier With Template 1", "payment_terms", "_Test Payment Term Template 1")
due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1") due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1")
self.assertEqual(due_date, "2016-02-29") self.assertEqual(due_date, "2016-02-29")
due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1") due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1")
self.assertEqual(due_date, "2017-02-28") self.assertEqual(due_date, "2017-02-28")
frappe.db.set_value("Supplier", "_Test Supplier With Template 1", "payment_terms", "") frappe.db.set_value("Supplier", "_Test Supplier With Template 1", "payment_terms", "")
# Set credit limit for the supplier group instead of supplier and evaluate the due date # Set credit limit for the supplier group instead of supplier and evaluate the due date
frappe.db.set_value("Supplier Group", "_Test Supplier Group", "payment_terms", "_Test Payment Term Template 3") frappe.db.set_value("Supplier Group", "_Test Supplier Group", "payment_terms", "_Test Payment Term Template 3")
due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1") due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1")
self.assertEqual(due_date, "2016-02-21") self.assertEqual(due_date, "2016-02-21")
# Payment terms for Supplier Group instead of supplier and evaluate the due date # Payment terms for Supplier Group instead of supplier and evaluate the due date
frappe.db.set_value("Supplier Group", "_Test Supplier Group", "payment_terms", "_Test Payment Term Template 1") frappe.db.set_value("Supplier Group", "_Test Supplier Group", "payment_terms", "_Test Payment Term Template 1")
# Leap year # Leap year
due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1") due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier With Template 1")
self.assertEqual(due_date, "2016-02-29") self.assertEqual(due_date, "2016-02-29")
# # Non Leap year # # Non Leap year
due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1") due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier With Template 1")
self.assertEqual(due_date, "2017-02-28") self.assertEqual(due_date, "2017-02-28")
# Supplier with no default Payment Terms Template # Supplier with no default Payment Terms Template
frappe.db.set_value("Supplier Group", "_Test Supplier Group", "payment_terms", "") frappe.db.set_value("Supplier Group", "_Test Supplier Group", "payment_terms", "")
frappe.db.set_value("Supplier", "_Test Supplier", "payment_terms", "") frappe.db.set_value("Supplier", "_Test Supplier", "payment_terms", "")
due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier") due_date = get_due_date("2016-01-22", "Supplier", "_Test Supplier")
self.assertEqual(due_date, "2016-01-22") self.assertEqual(due_date, "2016-01-22")
# # Non Leap year # # Non Leap year
due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier") due_date = get_due_date("2017-01-22", "Supplier", "_Test Supplier")
self.assertEqual(due_date, "2017-01-22") self.assertEqual(due_date, "2017-01-22")
def test_supplier_disabled(self): def test_supplier_disabled(self):
make_test_records("Item") make_test_records("Item")
frappe.db.set_value("Supplier", "_Test Supplier", "disabled", 1) frappe.db.set_value("Supplier", "_Test Supplier", "disabled", 1)
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
po = create_purchase_order(do_not_save=True) po = create_purchase_order(do_not_save=True)
self.assertRaises(PartyDisabled, po.save) self.assertRaises(PartyDisabled, po.save)
frappe.db.set_value("Supplier", "_Test Supplier", "disabled", 0) frappe.db.set_value("Supplier", "_Test Supplier", "disabled", 0)
po.save() po.save()
def test_supplier_country(self): def test_supplier_country(self):
# Test that country field exists in Supplier DocType # Test that country field exists in Supplier DocType
supplier = frappe.get_doc('Supplier', '_Test Supplier with Country') supplier = frappe.get_doc('Supplier', '_Test Supplier with Country')
self.assertTrue('country' in supplier.as_dict()) self.assertTrue('country' in supplier.as_dict())
# Test if test supplier field record is 'Greece' # Test if test supplier field record is 'Greece'
self.assertEqual(supplier.country, "Greece") self.assertEqual(supplier.country, "Greece")
# Test update Supplier instance country value # Test update Supplier instance country value
supplier = frappe.get_doc('Supplier', '_Test Supplier') supplier = frappe.get_doc('Supplier', '_Test Supplier')
supplier.country = 'Greece' supplier.country = 'Greece'
supplier.save() supplier.save()
self.assertEqual(supplier.country, "Greece") self.assertEqual(supplier.country, "Greece")
def test_party_details_tax_category(self): def test_party_details_tax_category(self):
from erpnext.accounts.party import get_party_details from erpnext.accounts.party import get_party_details
frappe.delete_doc_if_exists("Address", "_Test Address With Tax Category-Billing") frappe.delete_doc_if_exists("Address", "_Test Address With Tax Category-Billing")
# Tax Category without Address # Tax Category without Address
details = get_party_details("_Test Supplier With Tax Category", party_type="Supplier") details = get_party_details("_Test Supplier With Tax Category", party_type="Supplier")
self.assertEqual(details.tax_category, "_Test Tax Category 1") self.assertEqual(details.tax_category, "_Test Tax Category 1")
address = frappe.get_doc(dict( address = frappe.get_doc(dict(
doctype='Address', doctype='Address',
address_title='_Test Address With Tax Category', address_title='_Test Address With Tax Category',
tax_category='_Test Tax Category 2', tax_category='_Test Tax Category 2',
address_type='Billing', address_type='Billing',
address_line1='Station Road', address_line1='Station Road',
city='_Test City', city='_Test City',
country='India', country='India',
links=[dict( links=[dict(
link_doctype='Supplier', link_doctype='Supplier',
link_name='_Test Supplier With Tax Category' link_name='_Test Supplier With Tax Category'
)] )]
)).insert() )).insert()
# Tax Category with Address # Tax Category with Address
details = get_party_details("_Test Supplier With Tax Category", party_type="Supplier") details = get_party_details("_Test Supplier With Tax Category", party_type="Supplier")
self.assertEqual(details.tax_category, "_Test Tax Category 2") self.assertEqual(details.tax_category, "_Test Tax Category 2")
# Rollback # Rollback
address.delete() address.delete()
def create_supplier(**args): def create_supplier(**args):
args = frappe._dict(args) args = frappe._dict(args)
try: if frappe.db.exists("Supplier", args.supplier_name):
doc = frappe.get_doc({ return frappe.get_doc("Supplier", args.supplier_name)
"doctype": "Supplier",
"supplier_name": args.supplier_name,
"supplier_group": args.supplier_group or "Services",
"supplier_type": args.supplier_type or "Company",
"tax_withholding_category": args.tax_withholding_category
}).insert()
return doc doc = frappe.get_doc({
"doctype": "Supplier",
"supplier_name": args.supplier_name,
"supplier_group": args.supplier_group or "Services",
"supplier_type": args.supplier_type or "Company",
"tax_withholding_category": args.tax_withholding_category
}).insert()
except frappe.DuplicateEntryError: return doc
return frappe.get_doc("Supplier", args.supplier_name)

View File

@@ -142,6 +142,26 @@ def make_purchase_order(source_name, target_doc=None):
return doclist return doclist
@frappe.whitelist()
def make_purchase_invoice(source_name, target_doc=None):
doc = get_mapped_doc("Supplier Quotation", source_name, {
"Supplier Quotation": {
"doctype": "Purchase Invoice",
"validation": {
"docstatus": ["=", 1],
}
},
"Supplier Quotation Item": {
"doctype": "Purchase Invoice Item"
},
"Purchase Taxes and Charges": {
"doctype": "Purchase Taxes and Charges"
}
}, target_doc)
return doc
@frappe.whitelist() @frappe.whitelist()
def make_quotation(source_name, target_doc=None): def make_quotation(source_name, target_doc=None):
doclist = get_mapped_doc("Supplier Quotation", source_name, { doclist = get_mapped_doc("Supplier Quotation", source_name, {

View File

@@ -8,5 +8,15 @@ frappe.listview_settings['Supplier Quotation'] = {
} else if(doc.status==="Expired") { } else if(doc.status==="Expired") {
return [__("Expired"), "gray", "status,=,Expired"]; return [__("Expired"), "gray", "status,=,Expired"];
} }
},
onload: function(listview) {
listview.page.add_action_item(__("Purchase Order"), ()=>{
erpnext.bulk_transaction_processing.create(listview, "Supplier Quotation", "Purchase Order");
});
listview.page.add_action_item(__("Purchase Invoice"), ()=>{
erpnext.bulk_transaction_processing.create(listview, "Supplier Quotation", "Purchase Invoice");
});
} }
}; };

View File

@@ -49,7 +49,7 @@ valid_scorecard = [
"min_grade":0.0,"name":"Very Poor", "min_grade":0.0,"name":"Very Poor",
"prevent_rfqs":1, "prevent_rfqs":1,
"notify_supplier":0, "notify_supplier":0,
"doctype":"Supplier Scorecard Standing", "doctype":"Supplier Scorecard Scoring Standing",
"max_grade":30.0, "max_grade":30.0,
"prevent_pos":1, "prevent_pos":1,
"warn_pos":0, "warn_pos":0,
@@ -65,7 +65,7 @@ valid_scorecard = [
"name":"Poor", "name":"Poor",
"prevent_rfqs":1, "prevent_rfqs":1,
"notify_supplier":0, "notify_supplier":0,
"doctype":"Supplier Scorecard Standing", "doctype":"Supplier Scorecard Scoring Standing",
"max_grade":50.0, "max_grade":50.0,
"prevent_pos":0, "prevent_pos":0,
"warn_pos":0, "warn_pos":0,
@@ -81,7 +81,7 @@ valid_scorecard = [
"name":"Average", "name":"Average",
"prevent_rfqs":0, "prevent_rfqs":0,
"notify_supplier":0, "notify_supplier":0,
"doctype":"Supplier Scorecard Standing", "doctype":"Supplier Scorecard Scoring Standing",
"max_grade":80.0, "max_grade":80.0,
"prevent_pos":0, "prevent_pos":0,
"warn_pos":0, "warn_pos":0,
@@ -97,7 +97,7 @@ valid_scorecard = [
"name":"Excellent", "name":"Excellent",
"prevent_rfqs":0, "prevent_rfqs":0,
"notify_supplier":0, "notify_supplier":0,
"doctype":"Supplier Scorecard Standing", "doctype":"Supplier Scorecard Scoring Standing",
"max_grade":100.0, "max_grade":100.0,
"prevent_pos":0, "prevent_pos":0,
"warn_pos":0, "warn_pos":0,

View File

@@ -1,49 +0,0 @@
{
"add_more_button": 1,
"app": "ERPNext",
"creation": "2019-11-15 14:45:32.626641",
"docstatus": 0,
"doctype": "Onboarding Slide",
"domains": [],
"help_links": [
{
"label": "Learn More",
"video_id": "zsrrVDk6VBs"
}
],
"idx": 0,
"image_src": "",
"is_completed": 0,
"max_count": 3,
"modified": "2019-12-09 17:54:18.452038",
"modified_by": "Administrator",
"name": "Add A Few Suppliers",
"owner": "Administrator",
"ref_doctype": "Supplier",
"slide_desc": "",
"slide_fields": [
{
"align": "",
"fieldname": "supplier_name",
"fieldtype": "Data",
"label": "Supplier Name",
"placeholder": "",
"reqd": 1
},
{
"align": "",
"fieldtype": "Column Break",
"reqd": 0
},
{
"align": "",
"fieldname": "supplier_email",
"fieldtype": "Data",
"label": "Supplier Email",
"reqd": 1
}
],
"slide_order": 50,
"slide_title": "Add A Few Suppliers",
"slide_type": "Create"
}

View File

@@ -167,9 +167,14 @@ class AccountsController(TransactionBase):
validate_regional(self) validate_regional(self)
validate_einvoice_fields(self)
if self.doctype != 'Material Request': if self.doctype != 'Material Request':
apply_pricing_rule_on_transaction(self) apply_pricing_rule_on_transaction(self)
def before_cancel(self):
validate_einvoice_fields(self)
def on_trash(self): def on_trash(self):
# delete sl and gl entries on deletion of transaction # delete sl and gl entries on deletion of transaction
if frappe.db.get_single_value('Accounts Settings', 'delete_linked_ledger_entries'): if frappe.db.get_single_value('Accounts Settings', 'delete_linked_ledger_entries'):
@@ -402,6 +407,22 @@ class AccountsController(TransactionBase):
if item_qty != len(get_serial_nos(item.get('serial_no'))): if item_qty != len(get_serial_nos(item.get('serial_no'))):
item.set(fieldname, value) item.set(fieldname, value)
elif (
ret.get("pricing_rule_removed")
and value is not None
and fieldname
in [
"discount_percentage",
"discount_amount",
"rate",
"margin_rate_or_amount",
"margin_type",
"remove_free_item",
]
):
# reset pricing rule fields if pricing_rule_removed
item.set(fieldname, value)
if self.doctype in ["Purchase Invoice", "Sales Invoice"] and item.meta.get_field('is_fixed_asset'): if self.doctype in ["Purchase Invoice", "Sales Invoice"] and item.meta.get_field('is_fixed_asset'):
item.set('is_fixed_asset', ret.get('is_fixed_asset', 0)) item.set('is_fixed_asset', ret.get('is_fixed_asset', 0))
@@ -1313,6 +1334,9 @@ class AccountsController(TransactionBase):
payment_schedule['discount_type'] = schedule.discount_type payment_schedule['discount_type'] = schedule.discount_type
payment_schedule['discount'] = schedule.discount payment_schedule['discount'] = schedule.discount
if not schedule.invoice_portion:
payment_schedule['payment_amount'] = schedule.payment_amount
self.append("payment_schedule", payment_schedule) self.append("payment_schedule", payment_schedule)
def set_due_date(self): def set_due_date(self):
@@ -1542,13 +1566,12 @@ def validate_taxes_and_charges(tax):
tax.rate = None tax.rate = None
def validate_account_head(tax, doc): def validate_account_head(idx, account, company):
company = frappe.get_cached_value('Account', account_company = frappe.get_cached_value('Account', account, 'company')
tax.account_head, 'company')
if company != doc.company: if account_company != company:
frappe.throw(_('Row {0}: Account {1} does not belong to Company {2}') frappe.throw(_('Row {0}: Account {1} does not belong to Company {2}')
.format(tax.idx, frappe.bold(tax.account_head), frappe.bold(doc.company)), title=_('Invalid Account')) .format(idx, frappe.bold(account), frappe.bold(company)), title=_('Invalid Account'))
def validate_cost_center(tax, doc): def validate_cost_center(tax, doc):
@@ -1931,7 +1954,8 @@ def update_bin_on_delete(row, doctype):
qty_dict["ordered_qty"] = get_ordered_qty(row.item_code, row.warehouse) qty_dict["ordered_qty"] = get_ordered_qty(row.item_code, row.warehouse)
update_bin_qty(row.item_code, row.warehouse, qty_dict) if row.warehouse:
update_bin_qty(row.item_code, row.warehouse, qty_dict)
def validate_and_delete_children(parent, data): def validate_and_delete_children(parent, data):
deleted_children = [] deleted_children = []
@@ -2151,3 +2175,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
@erpnext.allow_regional @erpnext.allow_regional
def validate_regional(doc): def validate_regional(doc):
pass pass
@erpnext.allow_regional
def validate_einvoice_fields(doc):
pass

View File

@@ -70,9 +70,18 @@ class BuyingController(StockController, Subcontracting):
# set contact and address details for supplier, if they are not mentioned # set contact and address details for supplier, if they are not mentioned
if getattr(self, "supplier", None): if getattr(self, "supplier", None):
self.update_if_missing(get_party_details(self.supplier, party_type="Supplier", ignore_permissions=self.flags.ignore_permissions, self.update_if_missing(
doctype=self.doctype, company=self.company, party_address=self.supplier_address, shipping_address=self.get('shipping_address'), get_party_details(
fetch_payment_terms_template= not self.get('ignore_default_payment_terms_template'))) self.supplier,
party_type="Supplier",
doctype=self.doctype,
company=self.company,
party_address=self.get("supplier_address"),
shipping_address=self.get('shipping_address'),
fetch_payment_terms_template= not self.get('ignore_default_payment_terms_template'),
ignore_permissions=self.flags.ignore_permissions
)
)
self.set_missing_item_details(for_validate) self.set_missing_item_details(for_validate)
@@ -240,6 +249,7 @@ class BuyingController(StockController, Subcontracting):
"posting_time": self.get('posting_time'), "posting_time": self.get('posting_time'),
"qty": -1 * flt(d.get('stock_qty')), "qty": -1 * flt(d.get('stock_qty')),
"serial_no": d.get('serial_no'), "serial_no": d.get('serial_no'),
"batch_no": d.get("batch_no"),
"company": self.company, "company": self.company,
"voucher_type": self.doctype, "voucher_type": self.doctype,
"voucher_no": self.name, "voucher_no": self.name,
@@ -269,7 +279,8 @@ class BuyingController(StockController, Subcontracting):
"posting_date": self.posting_date, "posting_date": self.posting_date,
"posting_time": self.posting_time, "posting_time": self.posting_time,
"qty": -1 * d.consumed_qty, "qty": -1 * d.consumed_qty,
"serial_no": d.serial_no "serial_no": d.serial_no,
"batch_no": d.batch_no,
}) })
if rate > 0: if rate > 0:

Some files were not shown because too many files have changed in this diff Show More